MediaWiki:CharacterBuilder.js: Difference between revisions

From EQOA Sandstorm Wiki
Jump to navigationJump to search
(Created page with "mw.hook('wikipage.content').add(function () { var app = document.getElementById('character-builder-app'); if (!app) return; if (!window.EQOA_BUILDER_DATA) { app.innerHTML = '<b>Character Builder Error:</b> CharacterBuilderData.js did not load.'; return; } var data = window.EQOA_BUILDER_DATA; var baseStats = data.baseStats; var classes = data.classes; app.innerHTML = '<div class="character-builder-box">' + '<div class="character-builde...")
 
No edit summary
 
(219 intermediate revisions by 2 users not shown)
Line 1: Line 1:
mw.hook('wikipage.content').add(function () {
mw.hook('wikipage.content').add(function () {
  var app = document.getElementById('character-builder-app');
  if (!app) return;


  if (!window.EQOA_BUILDER_DATA) {
function loadGeneratedItemChunks() {
     app.innerHTML = '<b>Character Builder Error:</b> CharacterBuilderData.js did not load.';
     var chunkNumbers = [];
     return;
    var i;
  }
     var chunkCount = 5;
    var version = '20260601items5rangefix';


  var data = window.EQOA_BUILDER_DATA;
    for (i = 1; i <= chunkCount; i++) {
  var baseStats = data.baseStats;
        chunkNumbers.push(i);
  var classes = data.classes;
    }


  app.innerHTML =
    window.EQOA_GENERATED_ITEM_CHUNKS = [];
     '<div class="character-builder-box">' +
     window.EQOA_GENERATED_ITEMS = [];
      '<div class="character-builder-title">EQOA Character Builder</div>' +


      '<div class="character-builder-controls">' +
    return Promise.all(chunkNumbers.map(function (chunkNumber) {
        '<label>Race</label>' +
        return mw.loader.getScript(
        '<select id="cb-race"></select>' +
            mw.config.get('wgScriptPath') +
            '/index.php?title=MediaWiki:CharacterBuilderGeneratedItems' +
            chunkNumber +
            '.js&action=raw&ctype=text/javascript&v=' +
            version
        );
    })).then(function () {
        window.EQOA_GENERATED_ITEMS = [].concat.apply(
            [],
            window.EQOA_GENERATED_ITEM_CHUNKS || []
        ).filter(Boolean);


         '<label>Class</label>' +
         console.log(
        '<select id="cb-class"></select>' +
            'EQOA generated items loaded:',
      '</div>' +
            window.EQOA_GENERATED_ITEMS.length,
            'chunks:',
            window.EQOA_GENERATED_ITEM_CHUNKS.length
        );


      '<div class="character-builder-stats">' +
        console.log(
         '<div class="character-builder-section-title">Base Stats</div>' +
            'Throw items loaded:',
         '<table class="character-builder-stat-table">' +
            window.EQOA_GENERATED_ITEMS.filter(function (item) {
          '<tr><th>STR</th><th>STA</th><th>AGI</th><th>DEX</th><th>WIS</th><th>INT</th><th>CHA</th><th>HP</th><th>POW</th><th>AC</th></tr>' +
                return /throw/i.test(String(item.name || ''));
          '<tr>' +
            }).map(function (item) {
            '<td id="cb-str">0</td>' +
                return item.name + ' / ' + item.slot;
            '<td id="cb-sta">0</td>' +
            })
            '<td id="cb-agi">0</td>' +
        );
            '<td id="cb-dex">0</td>' +
 
            '<td id="cb-wis">0</td>' +
        window.dispatchEvent(new CustomEvent('eqoa-generated-items-ready'));
            '<td id="cb-int">0</td>' +
    });
             '<td id="cb-cha">0</td>' +
}
             '<td id="cb-hp">0</td>' +
 
             '<td id="cb-pow">0</td>' +
    function waitForBuilderData(attemptsLeft) {
             '<td id="cb-ac">0</td>' +
        if (window.EQOA_BUILDER_DATA && window.EQOA_GENERATED_ITEMS) {
          '</tr>' +
            initCharacterBuilder();
         '</table>' +
            return;
      '</div>' +
        }
     '</div>';
 
        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 classPathChoices = {};
 
        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"
];
 
var classPathTiers = [20, 30, 40, 49, 60];
 
var classPathOptionsByClass = {
    WAR: ['Bellow', 'Taunt'],
    PAL: ['Barrier', 'Taunt'],
    SK: ['Blood', 'Taunt'],
 
    CL: ['Stack Heal', 'Resist Buff'],
    DRD: ['AoE Stack', 'FR Debuff'],
    SHA: ['Pet Summon', 'Debuff'],
 
    BRD: ['Boon', 'Combat'],
    RAN: ['Melee Buff', 'Range Buff'],
    RGE: ['Wound', 'Avoidance'],
    ROG: ['Wound', 'Avoidance'],
    MNK: ['Melee Buff', 'Defense Buff'],
 
    ALC: ['Cocktails', 'Knack'],
    ENC: ['Boon', 'Mind'],
    MAG: ['Pet Buff', 'Stones'],
    NEC: ['Blood', 'Power'],
    WIZ: ['Clash', 'Power']
};
 
var classPathOptionsByClass = {
    WAR: ['Bellow', 'Taunt'],
    PAL: ['Barrier', 'Taunt'],
    SK: ['Blood', 'Taunt'],
 
    CL: ['Stack Heal', 'Resist Buff'],
    DRD: ['AoE Stack', 'FR Debuff'],
    SHA: ['Pet Summon', 'Debuff'],
 
    BRD: ['Boon', 'Combat'],
    RAN: ['Melee Buff', 'Range Buff'],
    RGE: ['Wound', 'Avoidance'],
    ROG: ['Wound', 'Avoidance'],
    MNK: ['Melee Buff', 'Defense Buff'],
 
    ALC: ['Cocktails', 'Knack'],
    ENC: ['Boon', 'Mind'],
    MAG: ['Pet Buff', 'Stones'],
    NEC: ['Blood', 'Power'],
    WIZ: ['Clash', 'Power']
};
 
 
 
 
        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>
 
<div class="cb-class-path-controls">
    <label>Class Path 20</label>
    <select id="cb-class-path-20"></select>
 
    <label>Class Path 30</label>
    <select id="cb-class-path-30"></select>
 
    <label>Class Path 40</label>
    <select id="cb-class-path-40"></select>
 
    <label>Class Path 49</label>
    <select id="cb-class-path-49"></select>
 
    <label>Class Path 60</label>
    <select id="cb-class-path-60"></select>
</div>
 
                    <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>
<span id="cb-export-level" style="display:none;"></span>
<span id="cb-export-class" style="display:none;"></span>
<span id="cb-export-master-class" style="display:none;"></span>
 
<div class="cb-export-character-sheet">
 
    <div class="cb-export-level-panel cb-export-panel">
        <div id="cb-export-level-title" class="cb-export-level-title">Level 60 Rogue</div>
    </div>
 
    <div class="cb-export-left-info-panel">
        <div class="cb-export-panel-title">General Info</div>
 
        <div class="cb-export-identity-stack">
 
<div>
    <b>Race / Transformation</b>
    <span id="cb-export-race-transform">-</span>
</div>
 
            <div>
                <b>Class Path</b>
                <span id="cb-export-class-path">None</span>
            </div>
 
            <div>
                <b>Exemplar Path</b>
                <span id="cb-export-exemplar-path">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">
                    <i></i><i></i><i></i><i></i><i></i>
                    <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">
                    <i></i><i></i><i></i><i></i><i></i>
                    <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 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>
</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');


  var raceSelect = document.getElementById('cb-race');
  var classSelect = document.getElementById('cb-class');


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


  function loadRaces() {
function switchBuilderTab(tabName) {
     raceSelect.innerHTML = '';
     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');
        }
    });


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


  function loadClassesForRace() {
    if (tabName === 'class-mastery') {
    var race = raceSelect.value;
        publishCharacterBuilderState();
     classSelect.innerHTML = '';
 
        setTimeout(function () {
            if (window.EQOA_SYNC_CLASS_MASTERY_FROM_BUILDER) {
                window.EQOA_SYNC_CLASS_MASTERY_FROM_BUILDER();
            }
        }, 0);
     }
}
 


    if (!baseStats[race]) return;
function getTransformationPath() {
    return transformationPathSelect ? transformationPathSelect.value : '';
}


     Object.keys(baseStats[race]).forEach(function (classCode) {
 
      var className = classes[classCode] || classCode;
        function addOption(select, value, label) {
      addOption(classSelect, classCode, classCode + ' - ' + className);
            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 getClassPathOptionsForClass(classCode, tier) {
    var options = classPathOptionsByClass[classCode] || [];
 
    if (parseInt(tier, 10) === 60) {
        return options.concat(['General']);
    }
 
    return options;
}
 
function getClassPathSelect(tier) {
    return document.getElementById('cb-class-path-' + tier);
}
 
function populateClassPathControls() {
    var classCode = classSelect ? classSelect.value : '';
    var level = getCharacterLevel();
 
    classPathTiers.forEach(function (tier) {
        var select = getClassPathSelect(tier);
        var options = getClassPathOptionsForClass(classCode, tier);
        var savedValue = classPathChoices[tier] || '';
 
        if (!select) return;
 
        select.innerHTML = '';
 
        addOption(select, '', level >= tier ? 'Choose Path' : 'Requires Lv ' + tier);
 
        options.forEach(function (optionName) {
            addOption(select, optionName, optionName);
        });
 
        if (level < tier) {
            select.value = '';
            select.disabled = true;
            classPathChoices[tier] = '';
        } else {
            select.disabled = false;
 
            if (options.indexOf(savedValue) !== -1) {
                select.value = savedValue;
            } else {
                select.value = '';
                classPathChoices[tier] = '';
            }
        }
     });
     });
}
function bindClassPathControls() {
    classPathTiers.forEach(function (tier) {
        var select = getClassPathSelect(tier);
        if (!select) return;
        select.addEventListener('change', function () {
            if (getCharacterLevel() < tier) {
                select.value = '';
                classPathChoices[tier] = '';
                return;
            }


            classPathChoices[tier] = select.value || '';
            publishCharacterBuilderState();
        });
    });
}
function getSelectedClassPathChoices() {
    var result = {};
    classPathTiers.forEach(function (tier) {
        var select = getClassPathSelect(tier);
        var value = select ? select.value : classPathChoices[tier];
        if (getCharacterLevel() >= tier && value) {
            result[tier] = value;
        }
    });
    return result;
}
function getSelectedClassPathSummary() {
    var choices = getSelectedClassPathChoices();
    var parts = [];
    classPathTiers.forEach(function (tier) {
        if (choices[tier]) {
            parts.push({
                tier: tier,
                value: choices[tier]
            });
        }
    });
    return parts;
}
        function getCurrentBuildData() {
            var gear = {};
gearSlots.forEach(function (slot) {
    if (equippedItems[slot]) {
        gear[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(),
cp: getSelectedClassPathChoices(),
    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;
classPathChoices = {};
populateClassPathControls();
    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();
     updateStats();
  }
}


  function updateStats() {
        function getAllLoadedItemsFlat() {
     var race = raceSelect.value;
            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.classPathChoices = buildData.classPathChoices || buildData.cp || {};
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.classPathChoices) {
    classPathChoices = buildData.classPathChoices;
    populateClassPathControls();
}
 
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 && item.slot || '').trim();
    var slotKey = slot.toLowerCase();
    var name = String(item && item.name || '').toLowerCase();
    var style = String(item && item.weaponstyle || '').toLowerCase();
 
    if (slotKey === 'helm') return 'Head';
    if (slotKey === 'helmet') return 'Head';
    if (slotKey === 'necklace') return 'Neck';
    if (slotKey === 'earring') return 'Ear';
    if (slotKey === 'leggings') return 'Legs';
    if (slotKey === 'leg') return 'Legs';
 
    if (slotKey === 'bow') return 'Range';
    if (slotKey === 'range') return 'Range';
    if (slotKey === 'ranged') return 'Range';
    if (slotKey === 'thrown') return 'Range';
    if (slotKey === 'throwing') return 'Range';
 
    if (
        style.indexOf('bow') !== -1 ||
        style.indexOf('crossbow') !== -1 ||
        style.indexOf('shortbow') !== -1 ||
        style.indexOf('longbow') !== -1 ||
        style.indexOf('throw') !== -1 ||
        style.indexOf('javelin') !== -1 ||
        name.indexOf('bow') !== -1 ||
        name.indexOf('crossbow') !== -1 ||
        name.indexOf('throw') !== -1 ||
        name.indexOf('javelin') !== -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) {
                var normalizedSlot;
 
                if (!generatedItem) return false;
 
                normalizedSlot = normalizeGeneratedSlot(generatedItem);
 
                if (slotType === "Primary") {
                    return normalizedSlot === "Primary" || normalizedSlot === "Secondary";
                }
 
                if (slotType === "Secondary") {
                    return normalizedSlot === "Secondary";
                }
 
                return normalizedSlot === 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>' +
    '</div>' +
 
    '<div class="cb-upgrade-row">' +
        '<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 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;
            }


    if (!baseStats[race] || !baseStats[race][classCode]) 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);


     var stats = baseStats[race][classCode];
if (upgradesOnlyToggle && upgradesOnlyToggle.checked) {
     filteredItems = filteredItems.filter(function (item) {
        return isItemUpgrade(slot, item);
    });
}


    document.getElementById('cb-str').textContent = stats.str || 0;
filteredItems = filteredItems.slice(0, 10);
    document.getElementById('cb-sta').textContent = stats.sta || 0;
    document.getElementById('cb-agi').textContent = stats.agi || 0;
    document.getElementById('cb-dex').textContent = stats.dex || 0;
    document.getElementById('cb-wis').textContent = stats.wis || 0;
    document.getElementById('cb-int').textContent = stats.int || 0;
    document.getElementById('cb-cha').textContent = stats.cha || 0;
    document.getElementById('cb-hp').textContent = stats.hp || 0;
    document.getElementById('cb-pow').textContent = stats.pow || 0;
    document.getElementById('cb-ac').textContent = stats.ac || 0;
  }


  raceSelect.addEventListener('change', loadClassesForRace);
                var currentItem = equippedItems[slot];
  classSelect.addEventListener('change', updateStats);


  loadRaces();
                picker.innerHTML = '';
  loadClassesForRace();
 
                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 cbHasValue(value) {
    return value !== undefined && value !== null && value !== 0 && value !== "";
}
function cbGetStatValue(item, key) {
    if (!item) return undefined;
    if (key === "dmg") {
        if (cbHasValue(item.dmg)) return item.dmg;
        if (cbHasValue(item.damage)) return item.damage;
    }
    return item[key];
}
function formatItemStats(item) {
    var rows = [];
function add(label, key) {
    var value = cbGetStatValue(item, key);
    if (cbHasValue(value)) {
        rows.push('<div><span>' + label + '</span> ' + value + '</div>');
    }
}
    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");
    return rows.join("");
}
function getItemTagHtml(item) {
    var tags = [];
    var lore = String(item.lore || "").trim().toUpperCase();
    var noTrade = String(item.notrade || item.noTrade || "").trim().toUpperCase();
    var trade = String(item.trade || "").trim().toUpperCase();
    if (noTrade === "NO TRADE" || trade === "1" || trade === "NO TRADE") {
        tags.push("NO TRADE");
    }
    if (lore === "LORE" || lore === "1") {
        tags.push("LORE");
    }
    if (!tags.length) return "";
    return '<div class="cb-tooltip-tags">' +
        tags.map(function (tag) {
            return '<span class="cb-tooltip-tag">' + tag + '</span>';
        }).join("") +
        '</div>';
}
function getClassText(item) {
    if (!item || !item.class) return "";
    if (Array.isArray(item.class)) {
        return item.class.join(", ");
    }
    return item.class;
}
function buildTooltipHtml(item) {
    var html = '';
    var classText = getClassText(item);
    html += '<div class="cb-tooltip-title cb-rarity-' +
        normalizeRarity(item.rarity) +
        '">' +
        item.name +
        '</div>';
    html += getItemTagHtml(item);
    if (cbHasValue(item.lvl)) {
        html += '<div class="cb-tooltip-line"><b>Level:</b> ' + item.lvl + '</div>';
    }
    if (item.slot) {
        html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + item.slot + '</div>';
    }
    if (cbHasValue(item.dur)) {
        html += '<div class="cb-tooltip-line"><b>DUR:</b> ' + item.dur + '</div>';
    }
    if (cbHasValue(item.gearhp)) {
        html += '<div class="cb-tooltip-line"><b>Gear HP:</b> ' + item.gearhp + '</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-line" style="margin-top:6px;"><b>Item Stats</b></div>';
    html += '<div class="cb-tooltip-stats">' + formatItemStats(item) + '</div>';
    return html;
}
function getCompareValue(newItem, oldItem, key) {
    var newValue = parseFloat(cbGetStatValue(newItem, key)) || 0;
    var oldValue = oldItem ? (parseFloat(cbGetStatValue(oldItem, key)) || 0) : 0;
    return newValue - oldValue;
}
function buildCompareTooltipHtml(slot, newItem) {
    var oldItem = equippedItems[slot];
    var html = '';
    var classText = getClassText(newItem);
    html += '<div class="cb-tooltip-title cb-rarity-' +
        normalizeRarity(newItem.rarity) +
        '">' +
        newItem.name +
        '</div>';
    html += getItemTagHtml(newItem);
    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 (cbHasValue(newItem.lvl)) {
        html += '<div class="cb-tooltip-line"><b>Level:</b> ' + newItem.lvl + '</div>';
    }
    if (newItem.slot) {
        html += '<div class="cb-tooltip-line"><b>Slot:</b> ' + newItem.slot + '</div>';
    }
    if (cbHasValue(newItem.dur)) {
        html += '<div class="cb-tooltip-line"><b>DUR:</b> ' + newItem.dur + '</div>';
    }
    if (cbHasValue(newItem.gearhp)) {
        html += '<div class="cb-tooltip-line"><b>Gear HP:</b> ' + newItem.gearhp + '</div>';
    }
    if (classText) {
        html += '<div class="cb-tooltip-line"><b>Class:</b> ' + classText + '</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("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");
    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 () {
        updateStats();
        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() : '';
function setExportText(id, value) {
    var el = document.getElementById(id);
    if (el) {
        el.textContent = value;
    }
}
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'
};
setExportText('cb-export-build-name', buildName || 'Unnamed Build');
setExportText(
    'cb-export-race-transform',
    (raceSelect.value || '-') + ', ' + (getTransformationPath() || 'None')
);
setExportText(
    'cb-export-class',
    data.classes && data.classes[classSelect.value]
        ? data.classes[classSelect.value]
        : (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';
}
setExportText('cb-export-level', 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('');
setExportText(
    'cb-export-hp-bar',
    document.getElementById('cb-hp') ? document.getElementById('cb-hp').textContent : '0'
);
setExportText(
    'cb-export-pow-bar',
    document.getElementById('cb-pow') ? document.getElementById('cb-pow').textContent : '0'
);
setExportText(
    'cb-export-ac-value',
    document.getElementById('cb-ac') ? document.getElementById('cb-ac').textContent : '0'
);
    var equipmentRows = [
    ['Head', 'Hands'],
    ['Ear1', 'Ear2'],
    ['Neck', 'Waist'],
    ['Bracelet1', 'Bracelet2'],
    ['Forearm', 'Bracelet2'],
    ['Chest', 'Legs'],
    ['Robe', 'Feet'],
    ['Ring1', 'Ring2'],
    ['Primary', 'Secondary'],
    ['Held1', 'Held2'],
    ['Range', '']
];
function getExportItemHtml(slot) {
    var item;
    if (!slot) return '';
    item = equippedItems[slot];
    if (!item) return '';
    return '<span class="cb-export-slot-label">' +
        slot +
        '</span> ' +
        '<span class="cb-rarity-' +
        normalizeRarity(item.rarity) +
        '">' +
        escapeExportHtml(item.name) +
        '</span>';
}
function rowHasAnyItem(row) {
    return row.some(function (slot) {
        return slot && equippedItems[slot];
    });
}
var itemsHtml = '';
equipmentRows.forEach(function (row) {
    if (!rowHasAnyItem(row)) return;
    if (row[0] === 'Bracelet1' && equippedItems.Forearm) return;
    if (row[0] === 'Forearm' && !equippedItems.Forearm) return;
    itemsHtml +=
        '<div class="cb-export-equipment-row">' +
            '<div>' + getExportItemHtml(row[0]) + '</div>' +
            '<div>' + getExportItemHtml(row[1]) + '</div>' +
        '</div>';
});
document.getElementById('cb-export-items').innerHTML =
    itemsHtml || '<div class="cb-export-empty">No equipment selected.</div>';
    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';
}
var exportLevelTitleBox = document.getElementById('cb-export-level-title');
if (exportLevelTitleBox) {
    exportLevelTitleBox.textContent =
        'Level ' +
        getCharacterLevel() +
        ' ' +
        (
            cmData && cmData.masterClassTitle
                ? cmData.masterClassTitle
                : (
                    data.classes && data.classes[classSelect.value]
                        ? data.classes[classSelect.value]
                        : classSelect.value
                )
        );
}
if (exportClassPathBox) {
    var classPathRows = getSelectedClassPathSummary();
    if (!classPathRows.length) {
        exportClassPathBox.textContent = 'None';
    } else {
        exportClassPathBox.innerHTML = classPathRows.map(function (row) {
            return '<div class="cb-export-class-path-row">' +
                '<b>Lv' + escapeExportHtml(row.tier) + '</b> ' +
                escapeExportHtml(row.value) +
            '</div>';
        }).join('');
    }
}
if (exportExemplarPathBox) {
    exportExemplarPathBox.textContent =
        cmData && cmData.exemplarPathChoice ? cmData.exemplarPathChoice : 'None';
}
if (exportTransformPathBox) {
    exportTransformPathBox.textContent = getTransformationPath() || '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';
        }
    );
    var hpBar = clonedDocument.querySelector('.cb-export-hp-row .cb-export-resource-bar');
    var powBar = clonedDocument.querySelector('.cb-export-pow-row .cb-export-resource-bar');
if (hpBar) {
    hpBar.style.setProperty('background-color', '#b00000', 'important');
    hpBar.style.setProperty('border-color', '#050505', 'important');
    Array.prototype.forEach.call(hpBar.querySelectorAll('i'), function (bubble) {
        bubble.style.setProperty('background-color', '#b00000', 'important');
    });
}
if (powBar) {
    powBar.style.setProperty('background-color', '#0070b8', 'important');
    powBar.style.setProperty('border-color', '#050505', 'important');
    Array.prototype.forEach.call(powBar.querySelectorAll('i'), function (bubble) {
        bubble.style.setProperty('background-color', '#0070b8', 'important');
    });
}
}
        }).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 getClassArchetype(classCode) {
    classCode = normalizeVitalClassKey(classCode);
    if (['WAR', 'PAL', 'SK'].indexOf(classCode) !== -1) return 'tank';
    if (['RGE', 'ROG', 'BRD', 'RAN', 'MNK', 'ALC'].indexOf(classCode) !== -1) return 'melee';
    if (['CL', 'DRD', 'SHA'].indexOf(classCode) !== -1) return 'priest';
    return 'caster';
}
function getBaseHpFactor(classCode) {
    var archetype = getClassArchetype(classCode);
    if (archetype === 'tank') return 24;
    if (archetype === 'melee') return 16;
    if (archetype === 'priest') return 13;
    return 10;
}
function getHeartyHpFactorBonus() {
    var selected = [];
    if (
        window.EQOA_CLASS_MASTERY_STATE &&
        typeof window.EQOA_CLASS_MASTERY_STATE.getSelectedMasteries === 'function'
    ) {
        selected = window.EQOA_CLASS_MASTERY_STATE.getSelectedMasteries();
    }
    return selected.reduce(function (total, name) {
        var match = String(name || '').match(/^Hearty\s+(\d+|I|II|III|IV|V)$/i);
        var rank;
        if (!match) return total;
        rank = parseInt(match[1], 10);
        if (isNaN(rank)) {
            rank = {
                I: 1,
                II: 2,
                III: 3,
                IV: 4,
                V: 5
            }[String(match[1]).toUpperCase()] || 0;
        }
        return total + rank;
    }, 0);
}
function calculateFormulaHp(classCode, level, stamina, itemHpBonus) {
    var hpFactor = getBaseHpFactor(classCode) + getHeartyHpFactorBonus();
    return Math.floor(
        (hpFactor + ((parseFloat(stamina) || 0) / 11)) *
        (parseInt(level, 10) || 1)
    ) + (parseInt(itemHpBonus, 10) || 0);
}
function getPowerStatsForClass(classCode) {
    classCode = normalizeVitalClassKey(classCode);
    if (['CL', 'DRD', 'SHA', 'PAL'].indexOf(classCode) !== -1) {
        return { primary: 'wis', secondary: 'cha' };
    }
    if (['WAR', 'SK', 'RGE', 'ROG', 'BRD', 'RAN', 'MNK', 'ALC'].indexOf(classCode) !== -1) {
        return { primary: 'dex', secondary: 'agi' };
    }
    return { primary: 'int', secondary: 'agi' };
}
function calculateFormulaPow(classCode, level, totalStats, itemPowBonus) {
    var powerStats = getPowerStatsForClass(classCode);
    var primary = parseFloat(totalStats[powerStats.primary]) || 0;
    var secondary = parseFloat(totalStats[powerStats.secondary]) || 0;
    return Math.floor(
        (28 + (primary / 10) + (secondary / 20)) *
        (parseInt(level, 10) || 1)
    ) + (parseInt(itemPowBonus, 10) || 0);
}
        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 itemHP = getItemVitalBonus(total.hp, base.hp || 0);
total.hp = calculateFormulaHp(
    classSelect.value,
    level,
    total.sta,
    itemHP
);
var itemPOW = getItemVitalBonus(total.pow, base.pow || 0);
total.pow = calculateFormulaPow(
    classSelect.value,
    level,
    total,
    itemPOW
);
            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 = [];
classPathChoices = {};
populateClassPathControls();
    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();
populateClassPathControls();
    updateStats();
});
levelInput.addEventListener('change', function () {
    getAvailableCmPoints();
    Object.keys(trainingStats).forEach(function (key) {
        clampTrainingInput(key);
    });
    removeInvalidEquippedItemsForLevel();
    enforceDisabledSlots();
    buildGearSlots();
populateClassPathControls();
    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();
bindClassPathControls();
populateClassPathControls();
tryLoadSharedBuildFromUrl();
loadAllSlotItems();
updateStats();
publishCharacterBuilderState();
    }
});
function openItemPage(itemName) {
    window.open(
        mw.config.get('wgArticlePath').replace('$1', encodeURIComponent(itemName)),
        '_blank'
    );
}

Latest revision as of 01:04, 1 June 2026

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

function loadGeneratedItemChunks() {
    var chunkNumbers = [];
    var i;
    var chunkCount = 5;
    var version = '20260601items5rangefix';

    for (i = 1; i <= chunkCount; i++) {
        chunkNumbers.push(i);
    }

    window.EQOA_GENERATED_ITEM_CHUNKS = [];
    window.EQOA_GENERATED_ITEMS = [];

    return Promise.all(chunkNumbers.map(function (chunkNumber) {
        return mw.loader.getScript(
            mw.config.get('wgScriptPath') +
            '/index.php?title=MediaWiki:CharacterBuilderGeneratedItems' +
            chunkNumber +
            '.js&action=raw&ctype=text/javascript&v=' +
            version
        );
    })).then(function () {
        window.EQOA_GENERATED_ITEMS = [].concat.apply(
            [],
            window.EQOA_GENERATED_ITEM_CHUNKS || []
        ).filter(Boolean);

        console.log(
            'EQOA generated items loaded:',
            window.EQOA_GENERATED_ITEMS.length,
            'chunks:',
            window.EQOA_GENERATED_ITEM_CHUNKS.length
        );

        console.log(
            'Throw items loaded:',
            window.EQOA_GENERATED_ITEMS.filter(function (item) {
                return /throw/i.test(String(item.name || ''));
            }).map(function (item) {
                return item.name + ' / ' + item.slot;
            })
        );

        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 classPathChoices = {};

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

var classPathTiers = [20, 30, 40, 49, 60];

var classPathOptionsByClass = {
    WAR: ['Bellow', 'Taunt'],
    PAL: ['Barrier', 'Taunt'],
    SK: ['Blood', 'Taunt'],

    CL: ['Stack Heal', 'Resist Buff'],
    DRD: ['AoE Stack', 'FR Debuff'],
    SHA: ['Pet Summon', 'Debuff'],

    BRD: ['Boon', 'Combat'],
    RAN: ['Melee Buff', 'Range Buff'],
    RGE: ['Wound', 'Avoidance'],
    ROG: ['Wound', 'Avoidance'],
    MNK: ['Melee Buff', 'Defense Buff'],

    ALC: ['Cocktails', 'Knack'],
    ENC: ['Boon', 'Mind'],
    MAG: ['Pet Buff', 'Stones'],
    NEC: ['Blood', 'Power'],
    WIZ: ['Clash', 'Power']
};

var classPathOptionsByClass = {
    WAR: ['Bellow', 'Taunt'],
    PAL: ['Barrier', 'Taunt'],
    SK: ['Blood', 'Taunt'],

    CL: ['Stack Heal', 'Resist Buff'],
    DRD: ['AoE Stack', 'FR Debuff'],
    SHA: ['Pet Summon', 'Debuff'],

    BRD: ['Boon', 'Combat'],
    RAN: ['Melee Buff', 'Range Buff'],
    RGE: ['Wound', 'Avoidance'],
    ROG: ['Wound', 'Avoidance'],
    MNK: ['Melee Buff', 'Defense Buff'],

    ALC: ['Cocktails', 'Knack'],
    ENC: ['Boon', 'Mind'],
    MAG: ['Pet Buff', 'Stones'],
    NEC: ['Blood', 'Power'],
    WIZ: ['Clash', 'Power']
};




        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>

<div class="cb-class-path-controls">
    <label>Class Path 20</label>
    <select id="cb-class-path-20"></select>

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

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

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

    <label>Class Path 60</label>
    <select id="cb-class-path-60"></select>
</div>

                    <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>
<span id="cb-export-level" style="display:none;"></span>
<span id="cb-export-class" style="display:none;"></span>
<span id="cb-export-master-class" style="display:none;"></span>

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

    <div class="cb-export-level-panel cb-export-panel">
        <div id="cb-export-level-title" class="cb-export-level-title">Level 60 Rogue</div>
    </div>

    <div class="cb-export-left-info-panel">
        <div class="cb-export-panel-title">General Info</div>

        <div class="cb-export-identity-stack">

<div>
    <b>Race / Transformation</b>
    <span id="cb-export-race-transform">-</span>
</div>

            <div>
                <b>Class Path</b>
                <span id="cb-export-class-path">None</span>
            </div>

            <div>
                <b>Exemplar Path</b>
                <span id="cb-export-exemplar-path">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">
                    <i></i><i></i><i></i><i></i><i></i>
                    <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">
                    <i></i><i></i><i></i><i></i><i></i>
                    <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 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>
</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 getClassPathOptionsForClass(classCode, tier) {
    var options = classPathOptionsByClass[classCode] || [];

    if (parseInt(tier, 10) === 60) {
        return options.concat(['General']);
    }

    return options;
}

function getClassPathSelect(tier) {
    return document.getElementById('cb-class-path-' + tier);
}

function populateClassPathControls() {
    var classCode = classSelect ? classSelect.value : '';
    var level = getCharacterLevel();

    classPathTiers.forEach(function (tier) {
        var select = getClassPathSelect(tier);
        var options = getClassPathOptionsForClass(classCode, tier);
        var savedValue = classPathChoices[tier] || '';

        if (!select) return;

        select.innerHTML = '';

        addOption(select, '', level >= tier ? 'Choose Path' : 'Requires Lv ' + tier);

        options.forEach(function (optionName) {
            addOption(select, optionName, optionName);
        });

        if (level < tier) {
            select.value = '';
            select.disabled = true;
            classPathChoices[tier] = '';
        } else {
            select.disabled = false;

            if (options.indexOf(savedValue) !== -1) {
                select.value = savedValue;
            } else {
                select.value = '';
                classPathChoices[tier] = '';
            }
        }
    });
}

function bindClassPathControls() {
    classPathTiers.forEach(function (tier) {
        var select = getClassPathSelect(tier);

        if (!select) return;

        select.addEventListener('change', function () {
            if (getCharacterLevel() < tier) {
                select.value = '';
                classPathChoices[tier] = '';
                return;
            }

            classPathChoices[tier] = select.value || '';
            publishCharacterBuilderState();
        });
    });
}

function getSelectedClassPathChoices() {
    var result = {};

    classPathTiers.forEach(function (tier) {
        var select = getClassPathSelect(tier);
        var value = select ? select.value : classPathChoices[tier];

        if (getCharacterLevel() >= tier && value) {
            result[tier] = value;
        }
    });

    return result;
}

function getSelectedClassPathSummary() {
    var choices = getSelectedClassPathChoices();
    var parts = [];

    classPathTiers.forEach(function (tier) {
        if (choices[tier]) {
            parts.push({
                tier: tier,
                value: choices[tier]
            });
        }
    });

    return parts;
}











        function getCurrentBuildData() {
            var gear = {};

gearSlots.forEach(function (slot) {
    if (equippedItems[slot]) {
        gear[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(),
cp: getSelectedClassPathChoices(),
    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;

classPathChoices = {};
populateClassPathControls();

    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.classPathChoices = buildData.classPathChoices || buildData.cp || {};
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.classPathChoices) {
    classPathChoices = buildData.classPathChoices;
    populateClassPathControls();
}

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 && item.slot || '').trim();
    var slotKey = slot.toLowerCase();
    var name = String(item && item.name || '').toLowerCase();
    var style = String(item && item.weaponstyle || '').toLowerCase();

    if (slotKey === 'helm') return 'Head';
    if (slotKey === 'helmet') return 'Head';
    if (slotKey === 'necklace') return 'Neck';
    if (slotKey === 'earring') return 'Ear';
    if (slotKey === 'leggings') return 'Legs';
    if (slotKey === 'leg') return 'Legs';

    if (slotKey === 'bow') return 'Range';
    if (slotKey === 'range') return 'Range';
    if (slotKey === 'ranged') return 'Range';
    if (slotKey === 'thrown') return 'Range';
    if (slotKey === 'throwing') return 'Range';

    if (
        style.indexOf('bow') !== -1 ||
        style.indexOf('crossbow') !== -1 ||
        style.indexOf('shortbow') !== -1 ||
        style.indexOf('longbow') !== -1 ||
        style.indexOf('throw') !== -1 ||
        style.indexOf('javelin') !== -1 ||
        name.indexOf('bow') !== -1 ||
        name.indexOf('crossbow') !== -1 ||
        name.indexOf('throw') !== -1 ||
        name.indexOf('javelin') !== -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) {
                var normalizedSlot;

                if (!generatedItem) return false;

                normalizedSlot = normalizeGeneratedSlot(generatedItem);

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

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

                return normalizedSlot === 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>' +
    '</div>' +

    '<div class="cb-upgrade-row">' +
        '<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 cbHasValue(value) {
    return value !== undefined && value !== null && value !== 0 && value !== "";
}

function cbGetStatValue(item, key) {
    if (!item) return undefined;

    if (key === "dmg") {
        if (cbHasValue(item.dmg)) return item.dmg;
        if (cbHasValue(item.damage)) return item.damage;
    }

    return item[key];
}

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

function add(label, key) {
    var value = cbGetStatValue(item, key);

    if (cbHasValue(value)) {
        rows.push('<div><span>' + label + '</span> ' + value + '</div>');
    }
}

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

    return rows.join("");
}

function getItemTagHtml(item) {
    var tags = [];
    var lore = String(item.lore || "").trim().toUpperCase();
    var noTrade = String(item.notrade || item.noTrade || "").trim().toUpperCase();
    var trade = String(item.trade || "").trim().toUpperCase();

    if (noTrade === "NO TRADE" || trade === "1" || trade === "NO TRADE") {
        tags.push("NO TRADE");
    }

    if (lore === "LORE" || lore === "1") {
        tags.push("LORE");
    }

    if (!tags.length) return "";

    return '<div class="cb-tooltip-tags">' +
        tags.map(function (tag) {
            return '<span class="cb-tooltip-tag">' + tag + '</span>';
        }).join("") +
        '</div>';
}

function getClassText(item) {
    if (!item || !item.class) return "";

    if (Array.isArray(item.class)) {
        return item.class.join(", ");
    }

    return item.class;
}

function buildTooltipHtml(item) {
    var html = '';
    var classText = getClassText(item);

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

    html += getItemTagHtml(item);

    if (cbHasValue(item.lvl)) {
        html += '<div class="cb-tooltip-line"><b>Level:</b> ' + item.lvl + '</div>';
    }

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

    if (cbHasValue(item.dur)) {
        html += '<div class="cb-tooltip-line"><b>DUR:</b> ' + item.dur + '</div>';
    }

    if (cbHasValue(item.gearhp)) {
        html += '<div class="cb-tooltip-line"><b>Gear HP:</b> ' + item.gearhp + '</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-line" style="margin-top:6px;"><b>Item Stats</b></div>';
    html += '<div class="cb-tooltip-stats">' + formatItemStats(item) + '</div>';

    return html;
}

function getCompareValue(newItem, oldItem, key) {
    var newValue = parseFloat(cbGetStatValue(newItem, key)) || 0;
    var oldValue = oldItem ? (parseFloat(cbGetStatValue(oldItem, key)) || 0) : 0;

    return newValue - oldValue;
}

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

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

    html += getItemTagHtml(newItem);

    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 (cbHasValue(newItem.lvl)) {
        html += '<div class="cb-tooltip-line"><b>Level:</b> ' + newItem.lvl + '</div>';
    }

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

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

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

    if (classText) {
        html += '<div class="cb-tooltip-line"><b>Class:</b> ' + classText + '</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("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");

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

        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() : '';
function setExportText(id, value) {
    var el = document.getElementById(id);
    if (el) {
        el.textContent = value;
    }
}
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'
};

setExportText('cb-export-build-name', buildName || 'Unnamed Build');

setExportText(
    'cb-export-race-transform',
    (raceSelect.value || '-') + ', ' + (getTransformationPath() || 'None')
);

setExportText(
    'cb-export-class',
    data.classes && data.classes[classSelect.value]
        ? data.classes[classSelect.value]
        : (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';
}

setExportText('cb-export-level', 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('');

setExportText(
    'cb-export-hp-bar',
    document.getElementById('cb-hp') ? document.getElementById('cb-hp').textContent : '0'
);

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

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

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

function getExportItemHtml(slot) {
    var item;

    if (!slot) return '';

    item = equippedItems[slot];

    if (!item) return '';

    return '<span class="cb-export-slot-label">' +
        slot +
        '</span> ' +
        '<span class="cb-rarity-' +
        normalizeRarity(item.rarity) +
        '">' +
        escapeExportHtml(item.name) +
        '</span>';
}

function rowHasAnyItem(row) {
    return row.some(function (slot) {
        return slot && equippedItems[slot];
    });
}

var itemsHtml = '';

equipmentRows.forEach(function (row) {
    if (!rowHasAnyItem(row)) return;

    if (row[0] === 'Bracelet1' && equippedItems.Forearm) return;
    if (row[0] === 'Forearm' && !equippedItems.Forearm) return;

    itemsHtml +=
        '<div class="cb-export-equipment-row">' +
            '<div>' + getExportItemHtml(row[0]) + '</div>' +
            '<div>' + getExportItemHtml(row[1]) + '</div>' +
        '</div>';
});

document.getElementById('cb-export-items').innerHTML =
    itemsHtml || '<div class="cb-export-empty">No equipment selected.</div>';


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

var exportLevelTitleBox = document.getElementById('cb-export-level-title');

if (exportLevelTitleBox) {
    exportLevelTitleBox.textContent =
        'Level ' +
        getCharacterLevel() +
        ' ' +
        (
            cmData && cmData.masterClassTitle
                ? cmData.masterClassTitle
                : (
                    data.classes && data.classes[classSelect.value]
                        ? data.classes[classSelect.value]
                        : classSelect.value
                )
        );
}

if (exportClassPathBox) {
    var classPathRows = getSelectedClassPathSummary();

    if (!classPathRows.length) {
        exportClassPathBox.textContent = 'None';
    } else {
        exportClassPathBox.innerHTML = classPathRows.map(function (row) {
            return '<div class="cb-export-class-path-row">' +
                '<b>Lv' + escapeExportHtml(row.tier) + '</b> ' +
                escapeExportHtml(row.value) +
            '</div>';
        }).join('');
    }
}

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

if (exportTransformPathBox) {
    exportTransformPathBox.textContent = getTransformationPath() || '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';
        }
    );

    var hpBar = clonedDocument.querySelector('.cb-export-hp-row .cb-export-resource-bar');
    var powBar = clonedDocument.querySelector('.cb-export-pow-row .cb-export-resource-bar');

if (hpBar) {
    hpBar.style.setProperty('background-color', '#b00000', 'important');
    hpBar.style.setProperty('border-color', '#050505', 'important');

    Array.prototype.forEach.call(hpBar.querySelectorAll('i'), function (bubble) {
        bubble.style.setProperty('background-color', '#b00000', 'important');
    });
}

if (powBar) {
    powBar.style.setProperty('background-color', '#0070b8', 'important');
    powBar.style.setProperty('border-color', '#050505', 'important');

    Array.prototype.forEach.call(powBar.querySelectorAll('i'), function (bubble) {
        bubble.style.setProperty('background-color', '#0070b8', 'important');
    });
}
}
        }).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 getClassArchetype(classCode) {
    classCode = normalizeVitalClassKey(classCode);

    if (['WAR', 'PAL', 'SK'].indexOf(classCode) !== -1) return 'tank';
    if (['RGE', 'ROG', 'BRD', 'RAN', 'MNK', 'ALC'].indexOf(classCode) !== -1) return 'melee';
    if (['CL', 'DRD', 'SHA'].indexOf(classCode) !== -1) return 'priest';

    return 'caster';
}

function getBaseHpFactor(classCode) {
    var archetype = getClassArchetype(classCode);

    if (archetype === 'tank') return 24;
    if (archetype === 'melee') return 16;
    if (archetype === 'priest') return 13;

    return 10;
}

function getHeartyHpFactorBonus() {
    var selected = [];

    if (
        window.EQOA_CLASS_MASTERY_STATE &&
        typeof window.EQOA_CLASS_MASTERY_STATE.getSelectedMasteries === 'function'
    ) {
        selected = window.EQOA_CLASS_MASTERY_STATE.getSelectedMasteries();
    }

    return selected.reduce(function (total, name) {
        var match = String(name || '').match(/^Hearty\s+(\d+|I|II|III|IV|V)$/i);
        var rank;

        if (!match) return total;

        rank = parseInt(match[1], 10);

        if (isNaN(rank)) {
            rank = {
                I: 1,
                II: 2,
                III: 3,
                IV: 4,
                V: 5
            }[String(match[1]).toUpperCase()] || 0;
        }

        return total + rank;
    }, 0);
}

function calculateFormulaHp(classCode, level, stamina, itemHpBonus) {
    var hpFactor = getBaseHpFactor(classCode) + getHeartyHpFactorBonus();

    return Math.floor(
        (hpFactor + ((parseFloat(stamina) || 0) / 11)) *
        (parseInt(level, 10) || 1)
    ) + (parseInt(itemHpBonus, 10) || 0);
}

function getPowerStatsForClass(classCode) {
    classCode = normalizeVitalClassKey(classCode);

    if (['CL', 'DRD', 'SHA', 'PAL'].indexOf(classCode) !== -1) {
        return { primary: 'wis', secondary: 'cha' };
    }

    if (['WAR', 'SK', 'RGE', 'ROG', 'BRD', 'RAN', 'MNK', 'ALC'].indexOf(classCode) !== -1) {
        return { primary: 'dex', secondary: 'agi' };
    }

    return { primary: 'int', secondary: 'agi' };
}

function calculateFormulaPow(classCode, level, totalStats, itemPowBonus) {
    var powerStats = getPowerStatsForClass(classCode);
    var primary = parseFloat(totalStats[powerStats.primary]) || 0;
    var secondary = parseFloat(totalStats[powerStats.secondary]) || 0;

    return Math.floor(
        (28 + (primary / 10) + (secondary / 20)) *
        (parseInt(level, 10) || 1)
    ) + (parseInt(itemPowBonus, 10) || 0);
}










        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 itemHP = getItemVitalBonus(total.hp, base.hp || 0);

total.hp = calculateFormulaHp(
    classSelect.value,
    level,
    total.sta,
    itemHP
);

var itemPOW = getItemVitalBonus(total.pow, base.pow || 0);

total.pow = calculateFormulaPow(
    classSelect.value,
    level,
    total,
    itemPOW
);

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

classPathChoices = {};
populateClassPathControls();

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

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

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

    removeInvalidEquippedItemsForLevel();
    enforceDisabledSlots();
    buildGearSlots();
populateClassPathControls();
    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();
bindClassPathControls();
populateClassPathControls();
tryLoadSharedBuildFromUrl();
loadAllSlotItems();
updateStats();
publishCharacterBuilderState();

    }

});

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