MediaWiki:CharacterBuilder.js: Difference between revisions

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


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




Line 409: Line 410:
     var equippedItems = {};
     var equippedItems = {};
     var itemCache = {};
     var itemCache = {};
     var pendingSharedBuild = null;
     var pendingSharedBuild = null;
     var sharedBuildApplied = false;
     var sharedBuildApplied = false;
Line 886: Line 886:


         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;
Line 1,833: Line 1,834:
tooltip.style.display = 'block';
tooltip.style.display = 'block';
moveTooltip(event);
moveTooltip(event);
}
}


Line 1,880: Line 1,882:
             slotCell.textContent = slot;
             slotCell.textContent = slot;
             slotCell.style.fontWeight = 'bold';
             slotCell.style.fontWeight = 'bold';


             var itemCell = document.createElement('td');
             var itemCell = document.createElement('td');
Line 2,336: Line 2,339:
         item.slot,
         item.slot,
         item.weaponstyle,
         item.weaponstyle,
         item.proc,
         item.proc,
         item.effect,
         item.effect,
Line 2,534: Line 2,536:


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


gearSlots.forEach(function (slot) {
gearSlots.forEach(function (slot) {
Line 2,733: Line 2,734:


}
}
}
 


     function removeInvalidEquippedItemsForLevel() {
     function removeInvalidEquippedItemsForLevel() {

Revision as of 14:59, 14 May 2026

ChatGPT





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


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

Generated image: Dark Elf raid tank build guide


Edit


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

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

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

Generated image: EQOA character builder overview


Edit


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


Image created
ΓÇó
Character builder export card tutorial

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



Image created
ΓÇó
Character builder export card tutorial

Stop

We just worked on Export Character Card feature


Character builder export card tutorial

Generated image: Character builder export card tutorial


Edit



Character builder export card tutorial

Generated image: Character builder export card tutorial


Edit


Okay next step?

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


Image created
ΓÇó
EQOA: Sandstorm character builder summary

Stop Render


EQOA: Sandstorm character builder summary

Generated image: EQOA: Sandstorm character builder summary


Edit


lets start with step 1


Image created
ΓÇó
EQOA: Sandstorm character builder layout

stop render


EQOA: Sandstorm character builder layout

Generated image: EQOA: Sandstorm character builder layout


Edit


I removed it, but its still showing up

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

Resist order should be

FR | AR
CR | PR
LR | DR

Vitals order should be 
HP
POW
AC

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

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

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

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

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

Look what you did, you broke the layout


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


Pasted text.txt
Document
this is my code

which function, i have 11 updateStats()

You want the REAL main stats recalculation function.

Search for:

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

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

Find something like:

updateWeaponSummary();
OR:

updateEquippedItems();
OR the final stat display updates.

Add the flash code RIGHT BEFORE the final closing:

}
So near the end it becomes:

updateWeaponSummary();

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

    if (!el) return;

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

    void el.offsetWidth;

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

}
DO NOT put it:

inside another loop

inside updateWeaponSummary()

inside updateEquippedItems()

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


done show me list

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

Export card polish

item icons

EQOA borders/frame art

watermark/logo

Search result highlighting

Slot icons (Head/Chest/Ring/etc)

Sticky search/filter bars

Animated overcap warnings

Theorycrafting / Gameplay
DPS estimator

Advanced stat weighting

Resist analyzer

Softcap efficiency warnings

Upgrade Path Mode expansion

Item Compare Window

Role Presets

Armor set bonuses

Proc synergy calculations

Build efficiency grading

QoL / Utility
Favorite Items / Wishlist

Recently Viewed Items

Saved local browser builds

Character save codes

Live URL updating

Additional global search filters:

rarity

level range

dropped by

zone

Wiki Ecosystem
Auto-link from item pages into builder

Embedded mini-builder on guides/pages

Public build gallery

Shared build comments/ratings

Loot-source upgrade recommendations

Technical / Long-Term
Builder analytics dashboard

External API support

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

Major Locked/Completed Systems
Shareable Build URLs ✅

Compact compressed URLs ✅

QR export/load system ✅

Character Export Card ✅

Build Notes export ✅

Compare Shared Builds ✅

Missing shared item warnings ✅

Reset Build ✅

Dark/Light themes ✅

Mobile layout polish ✅

Item compare tooltips ✅

Global item search ✅

Loot-source integration foundation ✅

Rarity system foundation ✅

Tooltip rarity borders/glows ✅

Animated compare stat highlights ✅

Weapon Summary tooltips ✅

Rarity colors across builder ✅

Legendary animated rarity effects ✅

Relic animated rarity effects ✅

Hover lift picker animation ✅

Equipped item glow polish ✅

Upgrade flash animation ✅

Best-in-Slot Helper ✅

Upgrades Only toggle ✅

Filters + Sorting ✅

Clickable item links everywhere ✅

Validation/auto-disable system ✅

Stat caps + overcap display ✅


Search result highlighting


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

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

Find this part near the end of updateStats():

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

}
        
Replace it with:

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

}
So you are deleting only one extra:

}
Also remove the duplicate:

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


After that: save → Ctrl + F5.


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

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

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

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

waitForBuilderData(30);

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

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

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

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

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

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

    function getTrainingPointsForLevel(level) {
        level = parseInt(level, 10);
        if (isNaN(level) || level < 1) level = 1;
        if (level > 60) level = 60;

        var tp = 20;

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

        return tp;
    }

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

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

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

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

                <label>Level</label>
                <input type="number" id="cb-level" min="1" max="60" value="60" style="width:70px;">
                <label>Build Name</label>
                <input type="text" id="cb-build-name" placeholder="Lv60 Ogre SK Raid Tank" style="width:220px;">
Build Notes

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


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

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

            <div class="character-builder-section-title" style="margin-top:15px;">Training Points</div>

            <table class="character-builder-stat-table">
                <tr>
                    <th>Available TP</th>
                    <th>Spent TP</th>
                    <th>Remaining TP</th>
                </tr>
                <tr>
                    <td id="cb-tp-available">482</td>
                    <td id="cb-tp-spent">0</td>
                    <td id="cb-tp-remaining">482</td>
                </tr>
            </table>

            <div class="character-builder-section-title" style="margin-top:18px;">Training Allocation</div>

            <table class="character-builder-stat-table">
                <tr>
                    <th>STR</th>
                    <th>STA</th>
                    <th>AGI</th>
                    <th>DEX</th>
                    <th>WIS</th>
                    <th>INT</th>
                    <th>CHA</th>
                </tr>
                <tr>
                    <td><input type="number" id="train-str" min="0" value="0" style="width:60px;"></td>
                    <td><input type="number" id="train-sta" min="0" value="0" style="width:60px;"></td>
                    <td><input type="number" id="train-agi" min="0" value="0" style="width:60px;"></td>
                    <td><input type="number" id="train-dex" min="0" value="0" style="width:60px;"></td>
                    <td><input type="number" id="train-wis" min="0" value="0" style="width:60px;"></td>
                    <td><input type="number" id="train-int" min="0" value="0" style="width:60px;"></td>
                    <td><input type="number" id="train-cha" min="0" value="0" style="width:60px;"></td>
                </tr>
            </table>

            <div class="character-builder-section-title" style="margin-top:18px;">Total Stats</div>

            <table class="character-builder-stat-table">
                <tr>
                    <th>STR</th><th>STA</th><th>AGI</th><th>DEX</th><th>WIS</th><th>INT</th><th>CHA</th>
                    <th>HP</th><th>POW</th><th>AC</th>
                    <th>PR</th><th>DR</th><th>FR</th><th>CR</th><th>LR</th><th>AR</th>
                </tr>
                <tr>
                    <td id="cb-str">0</td><td id="cb-sta">0</td><td id="cb-agi">0</td><td id="cb-dex">0</td>
                    <td id="cb-wis">0</td><td id="cb-int">0</td><td id="cb-cha">0</td>
                    <td id="cb-hp">0</td><td id="cb-pow">0</td><td id="cb-ac">0</td>
                    <td id="cb-pr">0</td><td id="cb-dr">0</td><td id="cb-fr">0</td><td id="cb-cr">0</td>
                    <td id="cb-lr">0</td><td id="cb-ar">0</td>
                </tr>
            </table>

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

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

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

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

                <div class="character-builder-gear-column">
                    <div class="character-builder-section-title" style="margin-top:18px;">Equipment</div>
                    <div id="cb-loading-message">Loading item categories...</div>
                    <div id="cb-gear-grid" class="character-builder-gear-grid"></div>
                </div>

            </div>




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

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

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

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

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

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

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

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

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

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

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

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

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

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

</div>




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

        </div>
    `;

    var raceSelect = document.getElementById('cb-race');
    var classSelect = document.getElementById('cb-class');
    var levelInput = document.getElementById('cb-level');
    var buildNameInput = document.getElementById('cb-build-name');
var buildNotesInput = document.getElementById('cb-build-notes');
var gearGrid = document.getElementById('cb-gear-grid');
var loadingMessage = document.getElementById('cb-loading-message');
var refreshButton = document.getElementById('cb-refresh-items');
var generateShareButton = document.getElementById('cb-generate-share-url');
var copyShareButton = document.getElementById('cb-copy-share-url');
var resetBuildButton = document.getElementById('cb-reset-build');
var exportCardButton = document.getElementById('cb-export-card');
var shareUrlBox = document.getElementById('cb-share-url-box');
var shareUrlOutput = document.getElementById('cb-share-url-output');
var shareUrlStatus = document.getElementById('cb-share-url-status');
var equippedSummaryBody = document.getElementById('cb-equipped-summary-body');
var warningBox = document.getElementById('cb-warning-box');
var globalItemSearchInput = document.getElementById('cb-global-item-search');
var globalSearchResultsBox = document.getElementById('cb-global-search-results');
var bisStatSelect = document.getElementById('cb-bis-stat');
var showBisButton = document.getElementById('cb-show-bis');
var bisResultsBox = document.getElementById('cb-bis-results');
var compareUrlAInput = document.getElementById('cb-compare-url-a');
var compareUrlBInput = document.getElementById('cb-compare-url-b');
var compareBuildsButton = document.getElementById('cb-compare-builds');
var compareResultsBox = document.getElementById('cb-compare-results');
var tooltip = document.getElementById('cb-item-tooltip');
var compareTooltip = document.getElementById('cb-compare-tooltip');
var exportCardInner = document.getElementById('cb-export-card-inner');
var exportQrBox = document.getElementById('cb-export-qr');
var exportNotesBox = document.getElementById('cb-export-notes');
var themeToggleButton = document.getElementById('cb-theme-toggle');

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


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

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

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

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

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

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

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

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

    function getCurrentBuildData() {
        var gear = {};

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

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

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

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

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

        return url;
    }

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

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

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

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

levelInput.value = 60;

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

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

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

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

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

buildGearSlots();
updateStats();
}

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

        Object.keys(itemCache).forEach(function (slotType) {

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

        return all;
    }

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

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

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

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

        return null;
    }

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

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

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

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

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

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

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

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

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

        equippedItems = {};

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

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

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

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

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

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

        pendingSharedBuild = decodeBuildData(encoded);

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

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

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

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

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

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

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

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

    function getPageWikitext(title) {
        return apiGet({
            action: "query",
            prop: "revisions",
            titles: title,
            rvprop: "content",
            rvslots: "main",
            formatversion: "2",
            format: "json"
        }).then(function (data) {
            var page = data.query.pages[0];

            if (!page || !page.revisions || !page.revisions[0]) return "";

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

            return page.revisions[0].content || "";
        });
    }

    function parseCategories(wikitext) {
        var categories = [];
        var regex = /\[\[Category:([^\]|]+)(?:\|[^\]]*)?\]\]/gi;
        var match;

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

        return categories;
    }

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

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

        lines.forEach(function (line) {
            line = line.trim();
            if (line.indexOf("|") !== 0) return;

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

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

            if (!key) return;

            item[key] = value;
        });

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

        item.categories = parseCategories(wikitext);

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

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

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

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

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

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

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

        if (item.class) {
            item.classList = item.class.split(",").map(function (x) {
                return x.trim().toUpperCase();
            });
        } else {
            item.classList = [];
        }

        return item;
    }

    function classCanUseItem(item, classCode) {
        if (!item.classList || item.classList.length === 0) return true;
        if (item.classList.indexOf("ALL") !== -1) return true;
        return item.classList.indexOf(classCode) !== -1;
    }

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

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

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

        return result;
    }

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

        var categories = getCategoriesForSlotType(slotType);

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

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

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

            pages = dedupePages(pages);

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

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

                itemCache[slotType] = items;
                return items;
            });
        });
    }

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

        var uniqueSlotTypes = [];

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

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

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

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

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

    function getDisabledSlots() {
        var disabled = {};

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

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

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

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

        return disabled;
    }

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

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

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

        var race = raceSelect.value;

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

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

        buildGearSlots();
        updateStats();
    }

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

        var disabledSlots = getDisabledSlots();

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

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

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

'' +
'' +
'';

            gearGrid.appendChild(slotBox);

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

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

if (!currentItem) return true;

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

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

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

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

return false;
}

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

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

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

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

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

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

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

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

    return true;
});

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

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

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

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

return filtered;
}

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

        if (!picker) return;

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

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

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

filteredItems = filteredItems.slice(0, 10);

            var currentItem = equippedItems[slot];

            picker.innerHTML = '';

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

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

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

            picker.appendChild(noneRow);

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

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

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

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

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

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

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

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

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

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

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

                picker.appendChild(row);
            });
        }

        rebuildPicker();

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

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

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

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

    function validateEquipment() {
        var warnings = [];

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

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

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

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

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

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

        return warnings;
    }

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

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

        if (!warningBox) return;

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

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

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

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

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

        return rows.join("");
    }

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

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

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

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

        return html;
    }

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

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

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

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

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

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

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

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

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

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

        var compareRows = [];

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

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

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

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

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

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

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

        return html;
    }

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

}

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

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

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

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

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

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

        equippedSummaryBody.innerHTML = '';

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

link.addEventListener('mousemove', moveTooltip);

link.addEventListener('mouseleave', hideTooltip);

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

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

            var effectText = '-';

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

            effectEl.textContent = effectText;
        }

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

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

    function getSpentTrainingPoints() {
        var total = 0;

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

        return total;
    }

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

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

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

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

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

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

        trainingStats[changedKey] = value;
    }

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

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

        value = parseFloat(value) || 0;

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

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

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

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

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

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

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

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

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

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

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

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

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

    if (!encoded) return null;

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

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

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

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

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

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

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

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

return html;
}

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

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

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

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

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

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

        if (!item) return;

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

return total;
}

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

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

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

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

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

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

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

return html;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

return "-";
}

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

if (!query) return [];

var allItems = getAllLoadedItemsFlat();

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

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

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

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

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

globalSearchResultsBox.style.display = "block";

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

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

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

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

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

globalSearchResultsBox.innerHTML = html;

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

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

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

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





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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

var itemsHtml = '';

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

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

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


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

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

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

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









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

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

populateExportCard();

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

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

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

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

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

        var level = getCharacterLevel();

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

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

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

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

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

        if (remainingTP < 0) remainingTP = 0;

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

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

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

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

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

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

if (!el) return;

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

void el.offsetWidth;

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

}


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

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

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

    raceSelect.addEventListener('change', loadClasses);

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

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

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

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

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

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

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

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

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

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

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

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

});

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


Close