MediaWiki:CharacterBuilder.js

From EQOA Sandstorm Wiki
Revision as of 05:38, 25 May 2026 by DevRenodor (talk | contribs)
Jump to navigationJump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
mw.hook('wikipage.content').add(function () {

    function loadGeneratedItemChunks() {
        window.EQOA_GENERATED_ITEM_CHUNKS = [];

        return Promise.all([
            1, 2, 3, 4, 5, 6, 7
        ].map(function (chunkNumber) {
            return mw.loader.getScript(
                mw.config.get('wgScriptPath') +
                '/index.php?title=MediaWiki:CharacterBuilderGeneratedItems' +
                chunkNumber +
                '.js&action=raw&ctype=text/javascript'
            );
        })).then(function () {
            window.EQOA_GENERATED_ITEMS = [].concat.apply(
                [],
                window.EQOA_GENERATED_ITEM_CHUNKS || []
            );

            window.dispatchEvent(new CustomEvent('eqoa-generated-items-ready'));
        });
    }

    function waitForBuilderData(attemptsLeft) {
        if (window.EQOA_BUILDER_DATA && window.EQOA_GENERATED_ITEMS) {
            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);
    }

    loadGeneratedItemChunks().then(function () {
    waitForBuilderData(100);
}).catch(function (error) {
    console.error('Generated item chunks failed to load.', error);

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

    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 sharedClassMasteries = [];

        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", "Robe", "Hands", "Legs", "Feet",
    "Forearm", "Bracelet1", "Bracelet2",
    "Waist", "Neck", "Ear1", "Ear2",
    "Ring1", "Ring2", "Primary", "Secondary",
    "Shield", "Held1", "Held2", "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="cb-builder-tabs">
    <button type="button" class="cb-tab-button cb-tab-active" data-cb-tab="equipment">Equipment</button>
    <button type="button" class="cb-tab-button" data-cb-tab="class-mastery">Class Mastery</button>
</div>

<div id="cb-tab-equipment" class="cb-tab-panel cb-tab-panel-active">

                <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>Available CMs</label>
<input type="number" id="cb-cm-points" min="0" max="1500" value="1500" style="width:80px;">

<br><label>Transformation Path</label> 
<select id="cb-transformation-path">
    <option value="">None</option>
    <option value="Werelion">Lionwere</option>
    <option value="Werebear">Bearwere</option>
    <option value="Werewolf">Wolfwere</option>
    <option value="Wererat">Ratwere</option>
    <option value="Weregator">Gatorwere</option>
    <option value="Vampire">Vampire</option>
    <option value="Were Hunter">Were-Hunter</option> 
</select>

                    <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-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: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 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:18px;">Character Summary</div>

<div class="cb-ingame-stat-panel">
    <div class="cb-ingame-stat-column">
        <div class="cb-ingame-row cb-ingame-header-row">
            <span>Training Points</span>
            <span></span>
            <span id="cb-tp-remaining">482</span>
        </div>

<span id="cb-tp-available" style="display:none;">482</span>
<span id="cb-tp-spent" style="display:none;">0</span>

        <div class="cb-ingame-row">
            <span>Strength</span>
            <input type="number" id="train-str" min="0" value="0">
            <span id="cb-str">0</span>
        </div>

        <div class="cb-ingame-row">
            <span>Stamina</span>
            <input type="number" id="train-sta" min="0" value="0">
            <span id="cb-sta">0</span>
        </div>

        <div class="cb-ingame-row">
            <span>Agility</span>
            <input type="number" id="train-agi" min="0" value="0">
            <span id="cb-agi">0</span>
        </div>

        <div class="cb-ingame-row">
            <span>Dexterity</span>
            <input type="number" id="train-dex" min="0" value="0">
            <span id="cb-dex">0</span>
        </div>

        <div class="cb-ingame-row">
            <span>Wisdom</span>
            <input type="number" id="train-wis" min="0" value="0">
            <span id="cb-wis">0</span>
        </div>

        <div class="cb-ingame-row">
            <span>Intelligence</span>
            <input type="number" id="train-int" min="0" value="0">
            <span id="cb-int">0</span>
        </div>

        <div class="cb-ingame-row">
            <span>Charisma</span>
            <input type="number" id="train-cha" min="0" value="0">
            <span id="cb-cha">0</span>
        </div>
    </div>

    <div class="cb-ingame-stat-column">
        <div class="cb-ingame-row">
            <span>AC</span>
            <span></span>
            <span id="cb-ac">0</span>
        </div>

        <div class="cb-ingame-row">
            <span>HP</span>
            <span></span>
            <span id="cb-hp">0</span>
        </div>

        <div class="cb-ingame-row">
            <span>Power</span>
            <span></span>
            <span id="cb-pow">0</span>
        </div>

        <div class="cb-ingame-row">
            <span>Fire</span>
            <span></span>
            <span id="cb-fr">48</span>
        </div>

        <div class="cb-ingame-row">
            <span>Ice</span>
            <span></span>
            <span id="cb-cr">48</span>
        </div>

        <div class="cb-ingame-row">
            <span>Lightning</span>
            <span></span>
            <span id="cb-lr">48</span>
        </div>

        <div class="cb-ingame-row">
            <span>Arcane</span>
            <span></span>
            <span id="cb-ar">48</span>
        </div>

        <div class="cb-ingame-row">
            <span>Poison</span>
            <span></span>
            <span id="cb-pr">48</span>
        </div>

        <div class="cb-ingame-row">
            <span>Disease</span>
            <span></span>
            <span id="cb-dr">48</span>
        </div>
    </div>
</div>







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

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

    <div class="character-builder-gear-column">
        <div class="character-builder-section-title cb-nested-section-title">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 id="cb-tab-class-mastery" class="cb-tab-panel">
    <div id="cm-calculator-app"></div>
</div>


<div id="cb-export-card-container" style="position:fixed; left:-99999px; top:0; width:1200px; min-width:1200px; height:auto; pointer-events:none; z-index:-9999;">
<div id="cb-export-card-inner" class="cb-export-card-dashboard">
    <div class="cb-export-title-main">EQOA: SANDSTORM</div>
    <div class="cb-export-title-sub">CHARACTER BUILDER</div>
    <div id="cb-export-build-name" class="cb-export-build-name">Unnamed Build</div>

    <div class="cb-export-character-sheet">

    <div class="cb-export-paperdoll-panel">
        <div class="cb-export-paperdoll-frame">
            <div class="cb-export-paperdoll-figure"></div>
            <img id="cb-export-class-icon" class="cb-export-class-icon" alt="">
        </div>
    </div>

    <div class="cb-export-left-info-panel">
        <div class="cb-export-identity-stack">
            <div><b>Level / Class</b><span><span id="cb-export-level"></span> / <span id="cb-export-class"></span></span></div>
            <div><b>Race / Transformation</b><span><span id="cb-export-race"></span> / <span id="cb-export-transform-path">None</span></span></div>
            <div><b>Exemplar Path</b><span id="cb-export-exemplar-path">None</span></div>
            <div><b>Class Path</b><span id="cb-export-class-path">None</span></div>
            <div><b>Master Class</b><span id="cb-export-master-class">None</span></div>
        </div>

        <div class="cb-export-bars">
            <div class="cb-export-resource-row cb-export-hp-row">
                <b>HP</b>
                <div class="cb-export-resource-bar"><span id="cb-export-hp-bar">0</span></div>
            </div>

            <div class="cb-export-resource-row cb-export-pow-row">
                <b>POW</b>
                <div class="cb-export-resource-bar"><span id="cb-export-pow-bar">0</span></div>
            </div>

            <div class="cb-export-ac-row">
                <b>AC</b>
                <span id="cb-export-ac-value">0</span>
            </div>
        </div>

        <div class="cb-export-stat-block">
            <div class="cb-export-small-title">Core Stats</div>
            <div id="cb-export-core-stats"></div>

            <div class="cb-export-small-title cb-export-resist-title">Resists</div>
            <div id="cb-export-resists"></div>
        </div>
    </div>

    <div class="cb-export-equipment-panel cb-export-panel">
        <div class="cb-export-panel-title">Equipment</div>
        <div id="cb-export-items"></div>
    </div>

    <div class="cb-export-cm-panel cb-export-panel">
        <div class="cb-export-panel-title">Class Mastery</div>
        <div id="cb-export-cm"></div>
    </div>

</div>

<div class="cb-export-panel cb-export-notes-panel">
    <div class="cb-export-panel-title">Build Notes</div>
    <div id="cb-export-notes" class="cb-export-notes-box"></div>
</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 exportCmBox = document.getElementById('cb-export-cm');
var transformationPathSelect = document.getElementById('cb-transformation-path');
        var raceSelect = document.getElementById('cb-race');
        var classSelect = document.getElementById('cb-class');
        var levelInput = document.getElementById('cb-level');
var cmPointsInput = document.getElementById('cb-cm-points');
        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 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 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 exportNotesBox = document.getElementById('cb-export-notes');
var exportCmBox = document.getElementById('cb-export-cm');
var exportMasterClassBox = document.getElementById('cb-export-master-class');
var exportClassPathBox = document.getElementById('cb-export-class-path');
var exportExemplarPathBox = document.getElementById('cb-export-exemplar-path');
var exportTransformPathBox = document.getElementById('cb-export-transform-path');
var themeToggleButton = document.getElementById('cb-theme-toggle');
var builderTabButtons = document.querySelectorAll('.cb-tab-button');
var builderTabPanels = document.querySelectorAll('.cb-tab-panel');



function switchBuilderTab(tabName) {
    Array.prototype.forEach.call(builderTabButtons, function (button) {
        if (button.getAttribute('data-cb-tab') === tabName) {
            button.classList.add('cb-tab-active');
        } else {
            button.classList.remove('cb-tab-active');
        }
    });

    Array.prototype.forEach.call(builderTabPanels, function (panel) {
        if (panel.id === 'cb-tab-' + tabName) {
            panel.classList.add('cb-tab-panel-active');
        } else {
            panel.classList.remove('cb-tab-panel-active');
        }
    });

    if (tabName === 'class-mastery') {
        publishCharacterBuilderState();

        setTimeout(function () {
            if (window.EQOA_SYNC_CLASS_MASTERY_FROM_BUILDER) {
                window.EQOA_SYNC_CLASS_MASTERY_FROM_BUILDER();
            }
        }, 0);
    }
}


function getTransformationPath() {
    return transformationPathSelect ? transformationPathSelect.value : '';
}


        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(),
    cm: getAvailableCmPoints(),
    p: getTransformationPath(),
    cms: (
        window.EQOA_CLASS_MASTERY_STATE &&
        typeof window.EQOA_CLASS_MASTERY_STATE.getSelectedMasteries === 'function'
    )
        ? window.EQOA_CLASS_MASTERY_STATE.getSelectedMasteries()
        : sharedClassMasteries,
    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 pageTitle = mw.config.get('wgPageName') || mw.config.get('wgTitle');
var scriptUrl = mw.config.get('wgScript');

if (scriptUrl.indexOf('http') !== 0) {
    scriptUrl = window.location.origin + scriptUrl;
}

var baseUrl = scriptUrl + '?title=' + encodeURIComponent(pageTitle);
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 {
                window.history.replaceState(null, document.title, url);
            } 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 (cmPointsInput) {
    cmPointsInput.value = getMaxCmPointsForLevel();
}


    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.cmPoints = buildData.cmPoints || buildData.cm;
buildData.trainingStats = buildData.trainingStats || buildData.t;
buildData.equippedItemNames = buildData.equippedItemNames || buildData.i;
buildData.transformationPath = buildData.transformationPath || buildData.p;
buildData.classMasteries = buildData.classMasteries || buildData.cms || [];
sharedClassMasteries = buildData.classMasteries || [];

            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.cmPoints !== undefined && cmPointsInput) {
    cmPointsInput.value = buildData.cmPoints;
    getAvailableCmPoints();
}

if (buildData.transformationPath !== undefined && transformationPathSelect) {
    transformationPathSelect.value = buildData.transformationPath;
}

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 getMaxCmPointsForLevel() {
    var max = getCharacterLevel() * 25;

    if (max > 1500) {
        max = 1500;
    }

    return max;
}

function getAvailableCmPoints() {
    var max = getMaxCmPointsForLevel();
    var value;

    if (!cmPointsInput) {
        return max;
    }

    cmPointsInput.max = max;

    value = parseInt(cmPointsInput.value, 10);

    if (isNaN(value)) {
        value = max;
    }

    if (value < 0) {
        value = 0;
    }

    if (value > max) {
        value = max;
    }

    cmPointsInput.value = value;

    return value;
}




        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|ItemPage)([\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 normalizeGeneratedSlot(item) {
    var slot = String(item.slot || '').trim();
    var name = String(item.name || '').toLowerCase();
    var style = String(item.weaponstyle || '').toLowerCase();

    if (
        slot.toLowerCase() === 'range' ||
        slot.toLowerCase() === 'ranged' ||
        style.indexOf('bow') !== -1 ||
        style.indexOf('crossbow') !== -1 ||
        style.indexOf('shortbow') !== -1 ||
        style.indexOf('longbow') !== -1 ||
        name.indexOf('bow') !== -1 ||
        name.indexOf('crossbow') !== -1
    ) {
        return 'Range';
    }

    return slot;
}

        function normalizeGeneratedItem(generatedItem, slotType) {
    var item = Object.assign({}, generatedItem);
item.slot = normalizeGeneratedSlot(item);

    if (!item.name) return null;

    if (Array.isArray(item.class)) {
        item.classList = item.class.map(function (x) {
            return String(x).trim().toUpperCase();
        });

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

    item.categories = item.categories || [slotType + " Items"];

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

    item.isShield = String(item.slot || "").toLowerCase() === "shield";
    item.isHeld = String(item.slot || "").toLowerCase() === "held";
    item.isSecondary = String(item.slot || "").toLowerCase() === "secondary";
item.isPrimary = String(item.slot || "").toLowerCase() === "primary";
item.isRange = String(item.slot || "").toLowerCase() === "range";

    statKeys.forEach(function (key) {
        item[key] = parseFloat(item[key]) || 0;
    });

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

    return item;
}

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

    if (window.EQOA_GENERATED_ITEMS && window.EQOA_GENERATED_ITEMS.length) {
        var items = window.EQOA_GENERATED_ITEMS
            .filter(function (generatedItem) {
                if (!generatedItem) return false;

var normalizedSlot = normalizeGeneratedSlot(generatedItem);

if (slotType === "Primary") {
    return normalizedSlot === "Primary" || normalizedSlot === "Secondary";
}

if (slotType === "Secondary") {
    return normalizedSlot === "Secondary";
}

return normalizedSlot === slotType;

return generatedItem.slot === slotType;
            })
            .map(function (generatedItem) {
                return normalizeGeneratedItem(generatedItem, slotType);
            })
            .filter(function (item) {
                return item !== null;
            });

        itemCache[slotType] = items;
        return Promise.resolve(items);
    }

    return Promise.reject("EQOA_GENERATED_ITEMS is not loaded.");
}

        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 secondary = equippedItems.Secondary;
    var shield = equippedItems.Shield;
    var held1 = equippedItems.Held1;
    var held2 = equippedItems.Held2;
    var range = equippedItems.Range;

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

    function disable(slot, reason) {
        if (!disabled[slot]) {
            disabled[slot] = reason;
        }
    }

    /* 2HD Primary locks off-hand style slots */
    if (primary && primary.is2HD) {
        disable("Secondary", "Disabled because a 2HD weapon is equipped in Primary.");
        disable("Shield", "Disabled because a 2HD weapon is equipped in Primary.");
        disable("Held1", "Disabled because a 2HD weapon is equipped in Primary.");
        disable("Held2", "Disabled because a 2HD weapon is equipped in Primary.");
        disable("Range", "Disabled because a 2HD weapon is equipped in Primary.");
    }

    /* Primary + Secondary uses both hands */
    if (primary && secondary) {
        disable("Held1", "Disabled because Primary and Secondary are both equipped.");
        disable("Held2", "Disabled because Primary and Secondary are both equipped.");
        disable("Shield", "Disabled because Primary and Secondary are both equipped.");
        disable("Range", "Disabled because Primary and Secondary are both equipped.");
    }

/* Primary uses main hand, blocking Held1 and Range */
if (primary) {
    disable("Held1", "Disabled because Primary is equipped.");
    disable("Range", "Disabled because Primary is equipped.");
}

/* Secondary uses off hand, blocking Held2 and Range */
if (secondary) {
    disable("Held2", "Disabled because Secondary is equipped.");
    disable("Range", "Disabled because Secondary is equipped.");
}

    /* Shield allows Held1, but blocks Held2 and Range */
    if (shield) {
        disable("Held2", "Disabled because a Shield is equipped.");
        disable("Range", "Disabled because a Shield is equipped.");
    }

    /* Held2 blocks Shield and Range */
    if (held2) {
        disable("Shield", "Disabled because Held2 is equipped.");
        disable("Range", "Disabled because Held2 is equipped.");
    }

/* Held1 blocks Primary and Range */
if (held1) {
    disable("Primary", "Disabled because Held1 is equipped.");
    disable("Range", "Disabled because Held1 is equipped.");
}

    /* Held1 + Held2 uses both hands */
    if (held1 && held2) {
        disable("Primary", "Disabled because Held1 and Held2 are both equipped.");
        disable("Secondary", "Disabled because Held1 and Held2 are both equipped.");
        disable("Shield", "Disabled because Held1 and Held2 are both equipped.");
        disable("Range", "Disabled because Held1 and Held2 are both equipped.");
    }

    /* Optional cleanup: Range conflicts with Shield / Held2 / dual wield */
    /* Range uses both weapon/off-hand space */
if (range) {
    disable("Primary", "Disabled because Range is equipped.");
    disable("Secondary", "Disabled because Range is equipped.");
    disable("Shield", "Disabled because Range is equipped.");
    disable("Held1", "Disabled because Range is equipped.");
    disable("Held2", "Disabled because Range is equipped.");
}
    

    /* Forearm vs Bracelet slots */
    if (forearm) {
        disable("Bracelet1", "Disabled because a Forearm item is equipped.");
        disable("Bracelet2", "Disabled because a Forearm item is equipped.");
    }

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

    return disabled;
}








        function isLoreItem(item) {
    if (!item) return false;

    return String(item.lore || "")
        .trim()
        .toUpperCase() === "LORE";
}

function isLoreConflict(item, currentSlot) {
    if (!item || !item.name) return false;
    if (!isLoreItem(item)) return false;

    var itemName = item.name.toLowerCase();

    for (var slot in equippedItems) {
        if (slot === currentSlot) continue;

        var equipped = equippedItems[slot];

        if (!equipped || !equipped.name) continue;

        if (equipped.name.toLowerCase() === itemName) {
            return true;
        }
    }

    return false;
}

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

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

    var equippedLoreNames = {};

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

        if (!item || !item.name) return;
        if (!isLoreItem(item)) return;

        var key = item.name.toLowerCase();

        if (equippedLoreNames[key]) {
            delete equippedItems[slot];
        } else {
            equippedLoreNames[key] = true;
        }
    });
}

        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 =
    '<div class="character-builder-section-title cb-slot-section-title">' + slot + '</div>' +
    '<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.isRange) return false;
    if (!item.isPrimary && !item.isSecondary) return false;
}

        if (slot === "Secondary") {
    if (item.isRange) return false;
    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) {
    warnings.push("Held items conflict because Primary and Secondary are both equipped.");
}

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

if (isLoreConflict(newItem, slot)) {
    html += '<div class="cb-tooltip-warning">LORE item already equipped</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 getItemVitalBonus(currentValue, baseValue) {
    currentValue = parseInt(currentValue, 10) || 0;
    baseValue = parseInt(baseValue, 10) || 0;

    var bonus = currentValue - baseValue;
    return bonus > 0 ? bonus : 0;
}



function normalizeVitalRaceKey(race) {
    race = String(race || '').toLowerCase();

    if (race.indexOf('human') !== -1 || race === 'hum') return 'Human';
    if (race.indexOf('barbarian') !== -1 || race === 'bar') return 'Barbarian';

    return race;
}

function normalizeVitalClassKey(classCode) {
    classCode = String(classCode || '').toUpperCase();

    if (classCode === 'ROG') return 'RGE';
    if (classCode === 'ROGUE') return 'RGE';
    if (classCode === 'WARRIOR') return 'WAR';
    if (classCode === 'PALADIN') return 'PAL';
    if (classCode === 'SHADOWKNIGHT') return 'SK';
    if (classCode === 'SHADOW KNIGHT') return 'SK';
    if (classCode === 'MAGICIAN') return 'MAG';
    if (classCode === 'SHAMAN') return 'SHA';

    return classCode;
}

function getConfiguredBaseVital(vitalKey, race, classCode, level) {
    var config = window.EQOA_CONFIG;
    var raceKey = normalizeVitalRaceKey(race);
    var classKey = normalizeVitalClassKey(classCode);

    if (parseInt(level, 10) !== 60) {
        return null;
    }

    if (
        !config ||
        !config.characterBuilderVitals ||
        !config.characterBuilderVitals.level60Base ||
        !config.characterBuilderVitals.level60Base[vitalKey] ||
        !config.characterBuilderVitals.level60Base[vitalKey][raceKey]
    ) {
        return null;
    }

    if (config.characterBuilderVitals.level60Base[vitalKey][raceKey][classKey] !== undefined) {
        return config.characterBuilderVitals.level60Base[vitalKey][raceKey][classKey];
    }

    return null;
}



function getClassMasteryBaseStats() {
    var base = getBaseStats() || {};
    var result = {};

    ['str', 'sta', 'agi', 'dex', 'wis', 'int', 'cha'].forEach(function (key) {
        result[key.toUpperCase()] =
            (parseInt(base[key], 10) || 0) +
            (parseInt(trainingStats[key], 10) || 0);
    });

    return result;
}

function publishCharacterBuilderState() {
    window.EQOA_CHARACTER_BUILDER_STATE = {
        getState: function () {
            return {
    race: raceSelect.value,
    classCode: classSelect.value,
    level: getCharacterLevel(),
    transformationPath: getTransformationPath(),
    cmAvailable: getAvailableCmPoints(),
    cmMax: getMaxCmPointsForLevel(),
    baseStats: getClassMasteryBaseStats(),
    selectedClassMasteries: sharedClassMasteries
};
        }
    };

    if (window.dispatchEvent) {
        window.dispatchEvent(new CustomEvent('eqoa-character-builder-state-change'));
    }
}





if (transformationPathSelect) {
    transformationPathSelect.addEventListener('change', function () {
        updateSummary();

        if (window.dispatchEvent) {
            window.dispatchEvent(new CustomEvent('eqoa-character-builder-state-change'));
        }
    });
}






        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;
    cap = parseFloat(cap) || 0;

    if (value > cap) {
        element.innerHTML =
            cap +
            '<span class="cb-stat-cap">(' + cap + ')</span>' +
            '<span class="cb-overcap">(+' + (value - cap) + ')</span>';
    } else {
        element.innerHTML =
            value +
            '<span class="cb-stat-cap">(' + cap + ')</span>';
    }
}

function getClassMasteryStatCaps() {
    var caps = {
        str: 400,
        sta: 400,
        agi: 400,
        dex: 400,
        wis: 400,
        int: 400,
        cha: 400
    };

    if (
        window.EQOA_CLASS_MASTERY_STATE &&
        typeof window.EQOA_CLASS_MASTERY_STATE.getStatCaps === 'function'
    ) {
        var cmCaps = window.EQOA_CLASS_MASTERY_STATE.getStatCaps();

        Object.keys(caps).forEach(function (key) {
            if (cmCaps[key] !== undefined) {
                caps[key] = parseInt(cmCaps[key], 10) || caps[key];
            }
        });
    }

    return caps;
}


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

        var buildData;

        if (!encoded) return null;

        buildData = decodeBuildData(encoded);

        if (!buildData) return null;

        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.cmPoints = buildData.cmPoints || buildData.cm;
        buildData.trainingStats = buildData.trainingStats || buildData.t;
        buildData.equippedItemNames = buildData.equippedItemNames || buildData.i;
        buildData.transformationPath = buildData.transformationPath || buildData.p;
        buildData.classMasteries = buildData.classMasteries || buildData.cms || [];

        return buildData;
    } catch (e) {
        console.error('Could not read build URL for comparison.', 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: 48,
    dr: 48,
    fr: 48,
    cr: 48,
    lr: 48,
    ar: 48
    };

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





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

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

function loadSavedBuilderTheme() {
    var isLight =
        document.documentElement.classList.contains('eqoa-theme-light') ||
        document.body.classList.contains('eqoa-theme-light');

    applyBuilderTheme(isLight ? 'light' : '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 escapeExportHtml(value) {
    return String(value == null ? '' : value)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}


function populateExportCard() {
    var buildName = buildNameInput ? buildNameInput.value.trim() : '';
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-hp-bar').textContent =
    document.getElementById('cb-hp') ? document.getElementById('cb-hp').textContent : '0';

document.getElementById('cb-export-pow-bar').textContent =
    document.getElementById('cb-pow') ? document.getElementById('cb-pow').textContent : '0';

document.getElementById('cb-export-ac-value').textContent =
    document.getElementById('cb-ac') ? document.getElementById('cb-ac').textContent : '0';

    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.';
    }
var cmState = window.EQOA_CLASS_MASTERY_STATE;
var cmData = cmState && typeof cmState.getExportData === 'function'
    ? cmState.getExportData()
    : null;

if (exportMasterClassBox) {
    exportMasterClassBox.textContent =
        cmData && cmData.masterClassTitle ? cmData.masterClassTitle : 'None';
}

if (exportClassPathBox) {
    exportClassPathBox.textContent = 'None';
}

if (exportExemplarPathBox) {
    exportExemplarPathBox.textContent =
        cmData && cmData.exemplarPathChoice ? cmData.exemplarPathChoice : 'None';
}

if (exportTransformPathBox) {
    exportTransformPathBox.textContent =
        cmData && cmData.path ? cmData.path : 'None';
}

function getExportCmRows(selectedMasteries) {
    var groups = {};
    var rows = [];

    function romanToNumber(value) {
        return {
            I: 1,
            II: 2,
            III: 3,
            IV: 4,
            V: 5
        }[String(value || '').toUpperCase()] || 0;
    }

    selectedMasteries.forEach(function (name) {
        var match = String(name || '').match(/^(.*)\s+(\d+|I|II|III|IV|V)$/i);
        var baseName;
        var rankText;
        var rankValue;

        if (!match) {
            rows.push({
                label: name,
                sort: name
            });
            return;
        }

        baseName = match[1];
        rankText = match[2];
        rankValue = parseInt(rankText, 10);

        if (isNaN(rankValue)) {
            rankValue = romanToNumber(rankText);
        }

        if (!groups[baseName]) {
            groups[baseName] = {
                baseName: baseName,
                ranks: []
            };
        }

        groups[baseName].ranks.push({
            text: rankText,
            value: rankValue
        });
    });

    Object.keys(groups).sort().forEach(function (baseName) {
        var group = groups[baseName];

        group.ranks.sort(function (a, b) {
            return a.value - b.value;
        });

        rows.push({
            label: group.baseName + ' ' + group.ranks.map(function (rank) {
                return rank.text;
            }).join(' > '),
            sort: group.baseName
        });
    });

    rows.sort(function (a, b) {
        return String(a.sort || '').localeCompare(String(b.sort || ''));
    });

    return rows;
}

if (exportCmBox) {
    if (!cmData) {
        exportCmBox.innerHTML = '<div>No Class Mastery data selected.</div>';
    } else {
        var html = '';
        var cmRows = getExportCmRows(cmData.selectedMasteries || []);
        var splitIndex = Math.ceil(cmRows.length / 2);
        var leftRows = cmRows.slice(0, splitIndex);
        var rightRows = cmRows.slice(splitIndex);

        function renderCmColumn(rows) {
            if (!rows.length) {
                return '<div>None selected.</div>';
            }

            return rows.map(function (row) {
                return '<div class="cb-export-cm-row">- ' + escapeExportHtml(row.label) + '</div>';
            }).join('');
        }

        html += '<div><b>CMs:</b> ' + cmData.spent + ' spent / ' + cmData.available + ' available</div>';
        html += '<div style="margin-top:10px;"><b>Selected Masteries</b></div>';

        html += '<div class="cb-export-cm-columns">' +
            '<div>' + renderCmColumn(leftRows) + '</div>' +
            '<div>' + renderCmColumn(rightRows) + '</div>' +
        '</div>';

        exportCmBox.innerHTML = html;
    }
}
}















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

    var exportContainer = document.getElementById('cb-export-card-container');

    if (exportContainer) {
    exportContainer.style.position = 'fixed';
    exportContainer.style.left = '-99999px';
    exportContainer.style.top = '0';
    exportContainer.style.width = '1200px';
    exportContainer.style.minWidth = '1200px';
    exportContainer.style.opacity = '1';
    exportContainer.style.pointerEvents = 'none';
    exportContainer.style.zIndex = '-9999';
    exportContainer.style.display = 'block';
}

    exportCardInner.style.width = '1200px';
    exportCardInner.style.minWidth = '1200px';
    exportCardInner.style.display = 'block';

    setTimeout(function () {
        html2canvas(exportCardInner, {
            backgroundColor: '#100a06',
            scale: 2,
            useCORS: true,
            allowTaint: true,
            logging: false,
            onclone: function (clonedDocument) {
                Array.prototype.forEach.call(
                    clonedDocument.querySelectorAll('#cb-export-card-inner *'),
                    function (el) {
                        el.style.backgroundImage = 'none';
                        el.style.webkitMaskImage = 'none';
                        el.style.maskImage = 'none';
                    }
                );
            }
        }).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();
        }).catch(function (error) {
            console.error('Export build card failed.', error);
            alert('Export failed. Check console for details.');
        });
    }, 150);
}


























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










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

            var level = getCharacterLevel();
var vitalsSection = document.getElementById('cb-player-vitals-section');

if (vitalsSection) {
    var showVitals =
        window.EQOA_CONFIG &&
        window.EQOA_CONFIG.features &&
        (
            window.EQOA_CONFIG.features.characterBuilderCalculatedHP ||
            window.EQOA_CONFIG.features.characterBuilderCalculatedPOW
        );

    vitalsSection.style.display = showVitals ? '' : 'none';
}

            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: 48,
    dr: 48,
    fr: 48,
    cr: 48,
    lr: 48,
    ar: 48
            };

            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;

            var statCaps = getClassMasteryStatCaps();

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


            var normalizedClassCode = normalizeVitalClassKey(classSelect.value);
            var hpClassMod = null;

            if (
                window.EQOA_CONFIG &&
                window.EQOA_CONFIG.features &&
                window.EQOA_CONFIG.features.characterBuilderCalculatedHP &&
                window.EQOA_CONFIG.characterBuilderVitals &&
                window.EQOA_CONFIG.characterBuilderVitals.hpClassMod
            ) {
                hpClassMod = window.EQOA_CONFIG.characterBuilderVitals.hpClassMod[normalizedClassCode];
            }

            if (hpClassMod !== null && hpClassMod !== undefined) {
                var itemHP = getItemVitalBonus(total.hp, base.hp || 0);

                total.hp =
                    (level * hpClassMod) +
                    (level * Math.round(total.sta / 11)) +
                    itemHP;
            }

            var configuredBasePOW = getConfiguredBaseVital(
                'POW',
                raceSelect.value,
                classSelect.value,
                level
            );

            if (
                window.EQOA_CONFIG &&
                window.EQOA_CONFIG.features &&
                window.EQOA_CONFIG.features.characterBuilderCalculatedPOW &&
                configuredBasePOW !== null
            ) {
                total.pow = configuredBasePOW + getItemVitalBonus(total.pow, base.pow || 0);
            }

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

            var playerHpBox = document.getElementById('cb-player-hp');
            var playerPowBox = document.getElementById('cb-player-pow');

            if (playerHpBox) playerHpBox.textContent = total.hp;
            if (playerPowBox) playerPowBox.textContent = total.pow;



            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();
if (!skipPublish) {
    publishCharacterBuilderState();
}

        }

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

function resetClassMasteryFromCharacterBuilder() {
    if (window.EQOA_RESET_CLASS_MASTERY) {
        window.EQOA_RESET_CLASS_MASTERY();
    }
}

function resetCharacterBuildAfterIdentityChange() {
    equippedItems = {};
    pendingSharedBuild = null;
    sharedBuildApplied = false;
    missingSharedItems = [];

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

    levelInput.value = 60;

    if (cmPointsInput) {
        cmPointsInput.value = getMaxCmPointsForLevel();
    }

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

    resetClassMasteryFromCharacterBuilder();
}

raceSelect.addEventListener('change', function () {
    resetCharacterBuildAfterIdentityChange();
    loadClasses();
    buildGearSlots();
    updateStats();
});

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

levelInput.addEventListener('change', function () {
    getAvailableCmPoints();

    Object.keys(trainingStats).forEach(function (key) {
        clampTrainingInput(key);
    });

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

if (cmPointsInput) {
cmPointsInput.addEventListener('change', function () {
        getAvailableCmPoints();
        publishCharacterBuilderState();
    });
}

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

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


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

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

/* Theme is controlled globally by MediaWiki:Common.js */


window.EQOA_EXPORT_COMBINED_BUILD_CARD = exportBuildCardImage;

if (exportCardButton) {
    exportCardButton.addEventListener('click', exportBuildCardImage);
}
Array.prototype.forEach.call(builderTabButtons, function (button) {
    button.addEventListener('click', function () {
        switchBuilderTab(button.getAttribute('data-cb-tab'));
    });
});


loadSavedBuilderTheme();
window.addEventListener('eqoa-class-mastery-state-change', function () {
    updateStats(true);
});
window.addEventListener('eqoa-wiki-theme-change', function (event) {
    if (event.detail && event.detail.theme) {
        applyBuilderTheme(event.detail.theme);
    }
});
        loadClasses();
        bindTrainingInputs();
        tryLoadSharedBuildFromUrl();
        loadAllSlotItems();
        updateStats();
        publishCharacterBuilderState();

    }

});

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