MediaWiki:ClassMasteryCalculator.js: Difference between revisions

From EQOA Sandstorm Wiki
Jump to navigationJump to search
No edit summary
No edit summary
 
(31 intermediate revisions by the same user not shown)
Line 45: Line 45:


         var data = window.EQOA_CM_DATA || {};
         var data = window.EQOA_CM_DATA || {};
        var selectedMasteries = {};
var selectedMasteries = {};
        var activeCmTab = 'General';
var sharedCmBuildApplied = false;
        var activeCmView = 'detailed';
var activeCmTab = 'General';
        var showLockedCms = true;
var activeCmView = 'detailed';
        var visibleMasteries = [];
var showLockedCms = true;
var visibleMasteries = [];


         function safeArray(value) {
         function safeArray(value) {
Line 105: Line 106:
                 return values;
                 return values;
             }
             }
function getAllHeadersContaining(cols, text) {
    var values = [];
    text = String(text || '').toLowerCase();
    headers.forEach(function (header, index) {
        var value;
        if (String(header || '').toLowerCase().indexOf(text) === -1) return;
        value = String(cols[index] || '').trim();
        if (!value) return;
        if (/^[0-9A-F]{4}$/i.test(value)) return;
        if (values.indexOf(value) === -1) {
            values.push(value);
        }
    });
    return values;
}


             function addStat(stats, cols, header, key) {
             function addStat(stats, cols, header, key) {
Line 111: Line 137:
                     stats[key] = value;
                     stats[key] = value;
                 }
                 }
            }
            function addStatAny(stats, cols, headers, key) {
                var found = false;
                headers.forEach(function (header) {
                    var value;
                    if (found) return;
                    value = parseInt(get(cols, header), 10);
                    if (!isNaN(value) && value !== 0) {
                        stats[key] = value;
                        found = true;
                    }
                });
             }
             }


             lines.forEach(function (line, index) {
             lines.forEach(function (line, index) {
                if (headerLineIndex === -1 && line.indexOf('Mastery\tDescription') === 0) {
if (
                    headerLineIndex = index;
    headerLineIndex === -1 &&
                }
    line.indexOf('Mastery\tDescription') !== -1
) {
    headerLineIndex = index;
}
             });
             });


Line 133: Line 179:
                 var cols;
                 var cols;
                 var name;
                 var name;
                var stats = {};
var stats = {};
                var baseStatReqs = {};
var baseStatReqs = {};
var cmId = '';


                 if (!line.trim()) return;
                 if (!line.trim()) return;


                cols = line.split('\t');
cols = line.split('\t');
                name = get(cols, 'Mastery');
name = get(cols, 'Mastery');


                if (!name) return;
cmId =
    get(cols, 'CmID') ||
    get(cols, 'CMID') ||
    get(cols, 'CM ID') ||
    get(cols, 'Cm ID');


                addStat(stats, cols, 'AC', 'ac');
if (!cmId && /^[0-9A-F]{4}$/i.test(String(cols[0] || '').trim())) {
                addStat(stats, cols, 'STR', 'str');
    cmId = String(cols[0] || '').trim();
                addStat(stats, cols, 'STA', 'sta');
}
                addStat(stats, cols, 'AGI', 'agi');
                addStat(stats, cols, 'DEX', 'dex');
                addStat(stats, cols, 'WIS', 'wis');
                addStat(stats, cols, 'INT', 'int');
                addStat(stats, cols, 'CHA', 'cha');
                addStat(stats, cols, 'HP Max', 'hp');
                addStat(stats, cols, 'PWR Max', 'pow');
                addStat(stats, cols, 'STR Max', 'strMax');
                addStat(stats, cols, 'STA Max', 'staMax');
                addStat(stats, cols, 'AGI Max', 'agiMax');
                addStat(stats, cols, 'DEX Max', 'dexMax');
                addStat(stats, cols, 'WIS Max', 'wisMax');
                addStat(stats, cols, 'INT Max', 'intMax');
                addStat(stats, cols, 'CHA Max', 'chaMax');


if (!name) return;
addStat(stats, cols, 'AC', 'ac');
addStat(stats, cols, 'STR', 'str');
addStat(stats, cols, 'STA', 'sta');
addStat(stats, cols, 'AGI', 'agi');
addStat(stats, cols, 'DEX', 'dex');
addStat(stats, cols, 'WIS', 'wis');
addStat(stats, cols, 'INT', 'int');
addStat(stats, cols, 'CHA', 'cha');
addStat(stats, cols, 'HP Max', 'hp');
addStat(stats, cols, 'PWR Max', 'pow');
addStat(stats, cols, 'POW Max', 'pow');
addStat(stats, cols, 'Power Max', 'pow');
/* Resists */
addStat(stats, cols, 'PR', 'pr');
addStat(stats, cols, 'DR', 'dr');
addStat(stats, cols, 'FR', 'fr');
addStat(stats, cols, 'CR', 'cr');
addStat(stats, cols, 'LR', 'lr');
addStat(stats, cols, 'AR', 'ar');
/* Regen / resource ticks */
addStat(stats, cols, 'HoT', 'hot');
addStat(stats, cols, 'HOT', 'hot');
addStat(stats, cols, 'PoT', 'pot');
addStat(stats, cols, 'POT', 'pot');
/* Modifiers */
addStat(stats, cols, 'HP Factor', 'hpFactor');
addStat(stats, cols, 'OFF Mod', 'offMod');
addStat(stats, cols, 'DEF Mod', 'defMod');
addStat(stats, cols, 'Speed', 'speed');
/* Stat cap increases */
addStatAny(stats, cols, ['STR Max', 'Max STR', 'STR MAX'], 'strMax');
addStatAny(stats, cols, ['STA Max', 'Max STA', 'STA MAX'], 'staMax');
addStatAny(stats, cols, ['AGI Max', 'Max AGI', 'AGI MAX'], 'agiMax');
addStatAny(stats, cols, ['DEX Max', 'Max DEX', 'DEX MAX'], 'dexMax');
addStatAny(stats, cols, ['WIS Max', 'Max WIS', 'WIS MAX'], 'wisMax');
addStatAny(stats, cols, ['INT Max', 'Max INT', 'INT MAX'], 'intMax');
addStatAny(stats, cols, ['CHA Max', 'Max CHA', 'CHA MAX'], 'chaMax');
                 ['STR', 'STA', 'DEX', 'AGI', 'INT', 'WIS', 'CHA'].forEach(function (stat) {
                 ['STR', 'STA', 'DEX', 'AGI', 'INT', 'WIS', 'CHA'].forEach(function (stat) {
                     var value = parseInt(get(cols, 'Base ' + stat + ' Req'), 10);
                     var value = parseInt(get(cols, 'Base ' + stat + ' Req'), 10);
Line 168: Line 249:
                 });
                 });


                rows.push({
rows.push({
                    name: name,
    id: cmId,
    name: name,
                     description: get(cols, 'Description'),
                     description: get(cols, 'Description'),
                     classification: get(cols, 'Classification'),
                     classification: get(cols, 'Classification'),
Line 177: Line 259:
     .concat(getAll(cols, 'Player Flags'))
     .concat(getAll(cols, 'Player Flags'))
     .concat(getAll(cols, 'Playerflag'))
     .concat(getAll(cols, 'Playerflag'))
     .concat(getAll(cols, 'Player Flag')),
     .concat(getAll(cols, 'Player Flag'))
    .concat(getAllHeadersContaining(cols, 'playerflag'))
    .concat(getAllHeadersContaining(cols, 'player flag')),
                     races: splitList(get(cols, 'Race')),
                     races: splitList(get(cols, 'Race')),
                     classes: splitList(get(cols, 'Class')),
                     classes: splitList(get(cols, 'Class')),
Line 214: Line 298:
         }
         }


        if ((!data.masteries || !data.masteries.length) && window.EQOA_CM_RAW) {
            data.masteries = buildMasteriesFromRaw(window.EQOA_CM_RAW);
        }


        function getBuilderState() {
function buildCmIdsByNameFromRaw(rawText) {
            if (
    var idsByName = {};
                window.EQOA_CHARACTER_BUILDER_STATE &&
    var lines = String(rawText || '').split(/\r?\n/);
                typeof window.EQOA_CHARACTER_BUILDER_STATE.getState === 'function'
 
            ) {
    lines.forEach(function (line) {
                return window.EQOA_CHARACTER_BUILDER_STATE.getState();
        var cols;
            }
        var id;
        var name;
 
        line = String(line || '').trim();
 
        if (!line) return;


             return null;
        if (line.indexOf('\t') !== -1) {
             cols = line.split('\t');
        } else if (line.indexOf('\\t') !== -1) {
            cols = line.split('\\t');
        } else {
            cols = line.split(/\s{2,}/);
         }
         }


function getSelectedTransformationPath() {
        id = String(cols[0] || '').trim().replace(/[^0-9A-F]/gi, '');
    var builderState = getBuilderState();
        name = String(cols[1] || '').trim();
 
        if (!/^[0-9A-F]{4}$/i.test(id)) return;
        if (!name || name.toLowerCase() === 'mastery') return;


    if (builderState && builderState.transformationPath !== undefined) {
        idsByName[name] = id;
        return builderState.transformationPath;
     });
     }


     return pathSelect ? pathSelect.value : '';
     return idsByName;
}
}


        function isEmbedded() {
if (window.EQOA_CM_RAW) {
            return !!getBuilderState();
    var rawMasteriesWithIds = buildMasteriesFromRaw(window.EQOA_CM_RAW);
    var cmIdsByName = buildCmIdsByNameFromRaw(window.EQOA_CM_IDS_RAW || window.EQOA_CM_RAW);
    var rawMasteriesByName = {};
 
    rawMasteriesWithIds.forEach(function (cm) {
        if (cm.name && cmIdsByName[cm.name]) {
            cm.id = cmIdsByName[cm.name];
         }
         }


         function getRaceCode(raceName) {
         if (cm.name) {
             var raceCodes = data.raceCodes || {};
             rawMasteriesByName[cm.name] = cm;
            return raceCodes[raceName] || raceName;
         }
         }
    });


         function listAllowsValue(list, value) {
    if (!data.masteries || !data.masteries.length) {
             var wanted = String(value || '').toUpperCase();
        data.masteries = rawMasteriesWithIds;
    } else {
         data.masteries.forEach(function (cm) {
             var rawCm;


             if (!wanted) return true;
             if (cm.name && cmIdsByName[cm.name]) {
                cm.id = cmIdsByName[cm.name];
            }


             return safeArray(list).some(function (entry) {
             rawCm = cm.name ? rawMasteriesByName[cm.name] : null;
                var text = String(entry || '').toUpperCase().trim();
                var tokens;


                if (!text || text === 'ALL') return true;
            if (rawCm && rawCm.stats) {
                cm.stats = cm.stats || {};


                 tokens = text.split(/\s+/);
                 Object.keys(rawCm.stats).forEach(function (statKey) {
                 return tokens.indexOf(wanted) !== -1;
                    cm.stats[statKey] = rawCm.stats[statKey];
             });
                 });
        }
             }
        });
    }


        function normalizePath(value) {
    if (window.EQOA_CM_DATA) {
            value = String(value || '').toLowerCase().replace(/[\s-]/g, '');
        window.EQOA_CM_DATA.masteries = data.masteries;
    }
}


            if (!value || value === 'all' || value === 'none') return '';


            var map = {
        function getBuilderState() {
                lionwere: 'werelion',
            if (
                 werelion: 'werelion',
                 window.EQOA_CHARACTER_BUILDER_STATE &&
                 bearwere: 'werebear',
                 typeof window.EQOA_CHARACTER_BUILDER_STATE.getState === 'function'
                werebear: 'werebear',
            ) {
                 wolfwere: 'werewolf',
                 return window.EQOA_CHARACTER_BUILDER_STATE.getState();
                werewolf: 'werewolf',
             }
                ratwere: 'wererat',
                wererat: 'wererat',
                gatorwere: 'weregator',
                weregator: 'weregator',
                vampire: 'vampire',
                werehunter: 'werehunter',
                hunterwere: 'werehunter',
                werebeast: 'werebeast'
             };


             return map[value] || value;
             return null;
         }
         }


        function isWerePath(path) {
function getSelectedTransformationPath() {
            return ['werelion', 'werebear', 'werewolf', 'wererat', 'weregator'].indexOf(path) !== -1;
    var builderState = getBuilderState();
        }


        function getBitePath(name) {
    if (builderState && builderState.transformationPath !== undefined) {
            name = String(name || '').toLowerCase();
        return builderState.transformationPath;
    }


            if (name.indexOf('lion bite') !== -1) return 'werelion';
    return pathSelect ? pathSelect.value : '';
            if (name.indexOf('bear bite') !== -1) return 'werebear';
}
            if (name.indexOf('wolf bite') !== -1) return 'werewolf';
            if (name.indexOf('rat bite') !== -1) return 'wererat';
            if (name.indexOf('gator bite') !== -1) return 'weregator';


             return '';
        function isEmbedded() {
             return !!getBuilderState();
         }
         }


         function isSharedWereMastery(name) {
         function getRaceCode(raceName) {
             return name === 'Lycanthropic Resistances' ||
            var raceCodes = data.raceCodes || {};
                name === 'Lycanthropic Regeneration' ||
             return raceCodes[raceName] || raceName;
                 name === 'Lycanthropic Rejuvenation' ||
        }
                name === 'Combat Senses' ||
 
                 name === 'Defensive Instincts';
        function listAllowsValue(list, value) {
            var wanted = String(value || '').toUpperCase();
 
            if (!wanted) return true;
 
            return safeArray(list).some(function (entry) {
                 var text = String(entry || '').toUpperCase().trim();
                var tokens;
 
                if (!text || text === 'ALL') return true;
 
                 tokens = text.split(/\s+/);
                return tokens.indexOf(wanted) !== -1;
            });
         }
         }


         function formatStatName(statName) {
         function normalizePath(value) {
            value = String(value || '').toLowerCase().replace(/[\s-]/g, '');
 
            if (!value || value === 'all' || value === 'none') return '';
 
             var map = {
             var map = {
                 ac: 'AC',
                 lionwere: 'werelion',
                 hp: 'HP',
                 werelion: 'werelion',
                 pow: 'Power',
                 bearwere: 'werebear',
                 str: 'STR',
                 werebear: 'werebear',
                 sta: 'STA',
                 wolfwere: 'werewolf',
                 agi: 'AGI',
                 werewolf: 'werewolf',
                 dex: 'DEX',
                 ratwere: 'wererat',
                 wis: 'WIS',
                 wererat: 'wererat',
                 int: 'INT',
                 gatorwere: 'weregator',
                 cha: 'CHA',
                 weregator: 'weregator',
                 strMax: 'STR Max',
                 vampire: 'vampire',
                 staMax: 'STA Max',
                 werehunter: 'werehunter',
                 agiMax: 'AGI Max',
                 hunterwere: 'werehunter',
                 dexMax: 'DEX Max',
                 werebeast: 'werebeast'
                wisMax: 'WIS Max',
                intMax: 'INT Max',
                chaMax: 'CHA Max'
             };
             };


             return map[statName] || String(statName || '').toUpperCase();
             return map[value] || value;
         }
         }


         function addOption(select, value, label) {
         function isWerePath(path) {
             var option = document.createElement('option');
             return ['werelion', 'werebear', 'werewolf', 'wererat', 'weregator'].indexOf(path) !== -1;
            option.value = value;
            option.textContent = label;
            select.appendChild(option);
         }
         }


         function getCmDisplayTab(cm) {
         function getBitePath(name) {
             var classification = String((cm && cm.classification) || '');
             name = String(name || '').toLowerCase();


             if (classification === 'Master Class') {
             if (name.indexOf('lion bite') !== -1) return 'werelion';
                if (safeArray(cm.races).length && safeArray(cm.races)[0] !== 'All') return 'Racial';
            if (name.indexOf('bear bite') !== -1) return 'werebear';
                if (safeArray(cm.classes).length && safeArray(cm.classes)[0] !== 'All') return 'Class';
            if (name.indexOf('wolf bite') !== -1) return 'werewolf';
            }
            if (name.indexOf('rat bite') !== -1) return 'wererat';
            if (name.indexOf('gator bite') !== -1) return 'weregator';


             return classification || 'General';
             return '';
         }
         }


         function getCmType(cm) {
         function isSharedWereMastery(name) {
             return getCmDisplayTab(cm) || 'Other';
             return name === 'Lycanthropic Resistances' ||
                name === 'Lycanthropic Regeneration' ||
                name === 'Lycanthropic Rejuvenation' ||
                name === 'Combat Senses' ||
                name === 'Defensive Instincts';
         }
         }


         function getCmTypeClass(cm) {
         function formatStatName(statName) {
             var type = getCmType(cm);
             var map = {
                ac: 'AC',
                hp: 'HP',
                pow: 'Power',
                str: 'STR',
                sta: 'STA',
                agi: 'AGI',
                dex: 'DEX',
                wis: 'WIS',
                int: 'INT',
                cha: 'CHA',
 
pr: 'PR',
dr: 'DR',
fr: 'FR',
cr: 'CR',
lr: 'LR',
ar: 'AR',
hot: 'HoT',
pot: 'PoT',
hpFactor: 'HP Factor',
offMod: 'OFF Mod',
defMod: 'DEF Mod',
speed: 'Speed',


            if (type === 'General') return 'cm-type-general';
                strMax: 'STR Max',
            if (type === 'Racial') return 'cm-type-racial';
                staMax: 'STA Max',
            if (type === 'Archetype') return 'cm-type-archetype';
                agiMax: 'AGI Max',
            if (type === 'Class') return 'cm-type-class';
                dexMax: 'DEX Max',
             if (type === 'Master Class') return 'cm-type-master';
                wisMax: 'WIS Max',
                intMax: 'INT Max',
                chaMax: 'CHA Max'
             };


             return 'cm-type-other';
             return map[statName] || String(statName || '').toUpperCase();
         }
         }


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


            if (builderState && builderState.baseStats) {
        function getCmDisplayTab(cm) {
                baseStats = builderState.baseStats;
             var classification = String((cm && cm.classification) || '');
             } else {
 
                baseStats = {
            if (classification === 'Master Class') {
                    STR: numberOrZero(document.getElementById('cm-base-str') && document.getElementById('cm-base-str').value),
                if (safeArray(cm.races).length && safeArray(cm.races)[0] !== 'All') return 'Racial';
                    STA: numberOrZero(document.getElementById('cm-base-sta') && document.getElementById('cm-base-sta').value),
                if (safeArray(cm.classes).length && safeArray(cm.classes)[0] !== 'All') return 'Class';
                    DEX: numberOrZero(document.getElementById('cm-base-dex') && document.getElementById('cm-base-dex').value),
                    AGI: numberOrZero(document.getElementById('cm-base-agi') && document.getElementById('cm-base-agi').value),
                    INT: numberOrZero(document.getElementById('cm-base-int') && document.getElementById('cm-base-int').value),
                    WIS: numberOrZero(document.getElementById('cm-base-wis') && document.getElementById('cm-base-wis').value),
                    CHA: numberOrZero(document.getElementById('cm-base-cha') && document.getElementById('cm-base-cha').value)
                };
             }
             }


             result = {
             return classification || 'General';
                STR: numberOrZero(baseStats.STR),
        }
                STA: numberOrZero(baseStats.STA),
 
                DEX: numberOrZero(baseStats.DEX),
        function getCmType(cm) {
                AGI: numberOrZero(baseStats.AGI),
            return getCmDisplayTab(cm) || 'Other';
                INT: numberOrZero(baseStats.INT),
        }
                WIS: numberOrZero(baseStats.WIS),
 
                CHA: numberOrZero(baseStats.CHA)
        function getCmTypeClass(cm) {
            };
            var type = getCmType(cm);


             Object.keys(selectedMasteries).forEach(function (name) {
             if (type === 'General') return 'cm-type-general';
                var stats = selectedMasteries[name].stats || {};
            if (type === 'Racial') return 'cm-type-racial';
                result.STR += numberOrZero(stats.str);
            if (type === 'Archetype') return 'cm-type-archetype';
                result.STA += numberOrZero(stats.sta);
            if (type === 'Class') return 'cm-type-class';
                result.DEX += numberOrZero(stats.dex);
             if (type === 'Master Class') return 'cm-type-master';
                result.AGI += numberOrZero(stats.agi);
                result.INT += numberOrZero(stats.int);
                result.WIS += numberOrZero(stats.wis);
                result.CHA += numberOrZero(stats.cha);
             });


             return result;
             return 'cm-type-other';
         }
         }


         function updateBaseStatInputsDisplay(baseStats) {
         function getBaseStatsFromInputs() {
            var map = {
                STR: 'cm-base-str',
                STA: 'cm-base-sta',
                DEX: 'cm-base-dex',
                AGI: 'cm-base-agi',
                INT: 'cm-base-int',
                WIS: 'cm-base-wis',
                CHA: 'cm-base-cha'
            };
 
            Object.keys(map).forEach(function (stat) {
                var input = document.getElementById(map[stat]);
                if (input) input.value = numberOrZero(baseStats[stat]);
            });
        }
 
        function getAvailablePoints() {
             var builderState = getBuilderState();
             var builderState = getBuilderState();
             var level = parseInt(levelInput.value, 10);
             var baseStats;
             var max;
             var result;
            var available;


             if (isNaN(level) || level < 1) level = 1;
             if (builderState && builderState.baseStats) {
             if (level > 60) level = 60;
                baseStats = builderState.baseStats;
 
             } else {
            levelInput.value = level;
                baseStats = {
            max = Math.min(level * 25, 1500);
                    STR: numberOrZero(document.getElementById('cm-base-str') && document.getElementById('cm-base-str').value),
 
                    STA: numberOrZero(document.getElementById('cm-base-sta') && document.getElementById('cm-base-sta').value),
            if (builderState && builderState.cmAvailable !== undefined) {
                    DEX: numberOrZero(document.getElementById('cm-base-dex') && document.getElementById('cm-base-dex').value),
                available = numberOrZero(builderState.cmAvailable);
                    AGI: numberOrZero(document.getElementById('cm-base-agi') && document.getElementById('cm-base-agi').value),
                if (available < 0) available = 0;
                    INT: numberOrZero(document.getElementById('cm-base-int') && document.getElementById('cm-base-int').value),
                if (available > max) available = max;
                    WIS: numberOrZero(document.getElementById('cm-base-wis') && document.getElementById('cm-base-wis').value),
                 return available;
                    CHA: numberOrZero(document.getElementById('cm-base-cha') && document.getElementById('cm-base-cha').value)
                 };
             }
             }


             return max;
             result = {
        }
                STR: numberOrZero(baseStats.STR),
 
                STA: numberOrZero(baseStats.STA),
        function getSpentPoints() {
                DEX: numberOrZero(baseStats.DEX),
             var total = 0;
                AGI: numberOrZero(baseStats.AGI),
                INT: numberOrZero(baseStats.INT),
                WIS: numberOrZero(baseStats.WIS),
                CHA: numberOrZero(baseStats.CHA)
             };


             Object.keys(selectedMasteries).forEach(function (name) {
             Object.keys(selectedMasteries).forEach(function (name) {
                 total += numberOrZero(selectedMasteries[name].points);
                 var stats = selectedMasteries[name].stats || {};
                result.STR += numberOrZero(stats.str);
                result.STA += numberOrZero(stats.sta);
                result.DEX += numberOrZero(stats.dex);
                result.AGI += numberOrZero(stats.agi);
                result.INT += numberOrZero(stats.int);
                result.WIS += numberOrZero(stats.wis);
                result.CHA += numberOrZero(stats.cha);
             });
             });


             return total;
             return result;
         }
         }


         function getCmPointStatusText() {
         function updateBaseStatInputsDisplay(baseStats) {
             var total = getAvailablePoints();
             var map = {
            var spent = getSpentPoints();
                STR: 'cm-base-str',
             var left = total - spent;
                STA: 'cm-base-sta',
                DEX: 'cm-base-dex',
                AGI: 'cm-base-agi',
                INT: 'cm-base-int',
                WIS: 'cm-base-wis',
                CHA: 'cm-base-cha'
             };


             if (left < 0) left = 0;
             Object.keys(map).forEach(function (stat) {
 
                var input = document.getElementById(map[stat]);
             return 'Total CMs: ' + total + ' | Spent: ' + spent + ' | Left: ' + left;
                if (input) input.value = numberOrZero(baseStats[stat]);
             });
         }
         }


         function getCmStatMinRequirements(cm) {
         function getAvailablePoints() {
             var reqs = [];
             var builderState = getBuilderState();
            var level = parseInt(levelInput.value, 10);
            var max;
            var available;


             if (!cm || !cm.baseStatReqs) return reqs;
             if (isNaN(level) || level < 1) level = 1;
            if (level > 60) level = 60;


             Object.keys(cm.baseStatReqs).forEach(function (statName) {
             levelInput.value = level;
                 var value = numberOrZero(cm.baseStatReqs[statName]);
            max = Math.min(level * 25, 1500);
                 if (value > 0) {
 
                    reqs.push({ stat: statName, value: value });
            if (builderState && builderState.cmAvailable !== undefined) {
                }
                 available = numberOrZero(builderState.cmAvailable);
                 if (available < 0) available = 0;
                if (available > max) available = max;
                return available;
            }
 
            return max;
        }
 
        function getSpentPoints() {
            var total = 0;
 
            Object.keys(selectedMasteries).forEach(function (name) {
                total += numberOrZero(selectedMasteries[name].points);
             });
             });


             return reqs;
             return total;
         }
         }


         function getCmLockReasons(cm, includePoints) {
        function getCmPointStatusText() {
            var total = getAvailablePoints();
            var spent = getSpentPoints();
            var left = total - spent;
 
            if (left < 0) left = 0;
 
            return 'Total CMs: ' + total + ' | Spent: ' + spent + ' | Left: ' + left;
        }
 
        function getCmStatMinRequirements(cm) {
            var reqs = [];
 
            if (!cm || !cm.baseStatReqs) return reqs;
 
            Object.keys(cm.baseStatReqs).forEach(function (statName) {
                var value = numberOrZero(cm.baseStatReqs[statName]);
                if (value > 0) {
                    reqs.push({ stat: statName, value: value });
                }
            });
 
            return reqs;
        }
 
         function getCmLockReasons(cm, includePoints) {
             var spent = getSpentPoints();
             var spent = getSpentPoints();
             var available = getAvailablePoints();
             var available = getAvailablePoints();
Line 536: Line 707:
         }
         }


        function getCmSearchText(cm) {
function getCmPassiveEffectText(cm) {
            var parts = [
    var fields = [
                cm.name,
        cm.effect,
                cm.description,
        cm.effects,
                cm.classification,
        cm.effectText,
                cm.lycanthropy
        cm.bonus,
            ];
        cm.passive,
 
        cm.description
            Object.keys(cm.stats || {}).forEach(function (key) {
    ];
                parts.push(key);
 
                parts.push(formatStatName(key));
    var found = '';
                parts.push(cm.stats[key]);
            });


            safeArray(cm.requirements).forEach(function (value) { parts.push(value); });
    fields.forEach(function (value) {
            safeArray(cm.spellsAdded).forEach(function (value) { parts.push(value); });
        value = String(value || '').trim();
            safeArray(cm.spellsRemoved).forEach(function (value) { parts.push(value); });


            return parts.join(' ').toLowerCase();
        if (!found && value && value.toLowerCase() !== 'none') {
            found = value;
         }
         }
    });


        function findCmByName(name) {
    return found;
            var found = null;
}


            safeArray(data.masteries).some(function (cm) {
function getCmGainLossHtml(cm) {
                if (cm.name === name) {
    var parts = [];
                    found = cm;
                    return true;
                }


                return false;
    Object.keys(cm.stats || {}).sort().forEach(function (key) {
            });
        var value = numberOrZero(cm.stats[key]);
        var className;


            return found;
        if (!value) return;
        }


         function isMasterClassTitle(cm) {
         className = value > 0 ? 'cm-card-gain' : 'cm-card-loss';
            return cm && cm.classification === 'Master Class';
        }


         function getSelectedMasterClassTitle() {
         parts.push(
    var selectedTitle = '';
            '<span class="' + className + '">' +
                escapeHtml(formatStatName(key)) + ' ' +
                (value > 0 ? '+' : '') +
                escapeHtml(value) +
            '</span>'
        );
    });
 
    safeArray(cm.spellsAdded).forEach(function (spell) {
        if (!spell) return;


    Object.keys(selectedMasteries).forEach(function (name) {
        parts.push(
        if (isMasterClassTitle(selectedMasteries[name])) {
            '<span class="cm-card-gain">+ ' +
             selectedTitle = name;
                escapeHtml(spell) +
         }
             '</span>'
         );
     });
     });


     return selectedTitle;
     safeArray(cm.spellsRemoved).forEach(function (spell) {
}
        if (!spell) return;


        parts.push(
            '<span class="cm-card-loss">- ' +
                escapeHtml(spell) +
            '</span>'
        );
    });


    if (!parts.length) {
        var passiveText = getCmPassiveEffectText(cm);


        if (passiveText) {
            parts.push(
                '<span class="cm-card-effect-note">' +
                    escapeHtml(passiveText) +
                '</span>'
            );
        }
    }


if (cm.description) {
    parts.unshift(
        '<span class="cm-card-description-chip">' +
            escapeHtml(cm.description) +
        '</span>'
    );
}


    if (!parts.length) return '';


    return '<div class="cm-card-effects">' + parts.join('') + '</div>';
}


function getCmPlayerFlags(cm) {
        function getCmSearchText(cm) {
    var flags = [];
            var parts = [
                cm.name,
                cm.description,
                cm.classification,
                cm.lycanthropy
            ];


    if (!cm) return flags;
            Object.keys(cm.stats || {}).forEach(function (key) {
 
                parts.push(key);
    safeArray(cm.playerFlags).forEach(function (flag) {
                parts.push(formatStatName(key));
        flags.push(flag);
                parts.push(cm.stats[key]);
    });
            });
 
            safeArray(cm.requirements).forEach(function (value) { parts.push(value); });
            safeArray(cm.spellsAdded).forEach(function (value) { parts.push(value); });
            safeArray(cm.spellsRemoved).forEach(function (value) { parts.push(value); });


    Object.keys(cm).forEach(function (key) {
            return parts.join(' ').toLowerCase();
        if (String(key).toLowerCase().indexOf('playerflag') !== -1) {
            if (typeof cm[key] === 'string') {
                flags.push(cm[key]);
            } else {
                safeArray(cm[key]).forEach(function (flag) {
                    flags.push(flag);
                });
            }
         }
         }
    });


    return flags;
}


function getRomanRank(value) {
    var match = String(value || '').trim().match(/\s+(I|II|III|IV|V)$/i);


    if (!match) return 0;


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


function stripRankSuffix(value) {
    return String(value || '')
        .trim()
        .replace(/\s+(I|II|III|IV|V)$/i, '');
}


function normalizePathKey(value) {
    return stripRankSuffix(value)
        .toLowerCase()
        .replace(/[^a-z0-9]/g, '');
}


function getPathFlag(cm) {
    var found = '';


    getCmPlayerFlags(cm).forEach(function (flag) {
        var text = String(flag || '').trim();


        if (
function findCmByNameOrId(value) {
            !found &&
    var found = null;
            (
    var wanted = String(value || '').trim().toLowerCase();
                text.toLowerCase().indexOf('exemplar') !== -1 ||
 
                text.toLowerCase().indexOf('path') !== -1
    if (!wanted) return null;
            )
 
         ) {
    safeArray(data.masteries).some(function (cm) {
             found = text;
        var cmName = String(cm.name || '').trim().toLowerCase();
         }
        var cmId = String(cm.id || cm.cmId || cm.CmID || '').trim().toLowerCase();
     });
 
         if (cmName === wanted || cmId === wanted) {
             found = cm;
            return true;
         }
 
        return false;
     });


     return found;
     return found;
}
}


function isClassPathCm(cm) {
var masterClassTitles = [
     if (!cm) return false;
     'Berserker',
    'Chosen',
    'Deputy',
    'Doomseeker',
    'Immortal',
    'Juggernaut',
    'Mercenary',
    'Savage',
    'Scholar',
    'Tinker',
 
    'Arcane Were-Hunter',
    'Divine Were-Hunter',
    'Gypsy Were-Hunter',
    'Heroic Were-Hunter',
    'Metabolist',
    'Transmuter',
    'Gypsy',
    'Minstrel',
    'Exorcist',
    'Faithful',
    'Storm Caller',
    'Wilding',
    'Animator',
 
    'Spellbinder',
    'Aeromancer',
    'Geomancer',
    'Hydromancer',
    'Pyromancer',
    'Ninja',
    'Sensei',


     if (getPathFlag(cm)) return true;
     'Defiler',
    'Lich',
    'Crusader',
    'Guardian',
    'Forester',
    'Hunter',
    'Assassin',
    'Burglar',
    'Death Knight',
    'Slayer',


     return getCmType(cm) === 'Class' && getRomanRank(cm.name) > 0;
     'Elder',
}
    'Mystic',
    'Defender',


function getClassPathKey(cm) {
    'Hero',
     var flag = getPathFlag(cm);
    'Channeler',
     'Sorcerer',


     if (flag) {
     'Bravo',
        return normalizePathKey(flag);
    'Diviner',
     }
    'Sage',
     'Soldier'
];


     return normalizePathKey(cm && cm.name);
function normalizeMasterClassName(value) {
     return String(value || '')
        .toLowerCase()
        .replace(/[\s\u00a0]+/g, ' ')
        .trim();
}
}


function getSelectedClassPathKey() {
function isMasterClassTitle(cm) {
     var selectedKey = '';
     var cmName;
 
    if (!cm || !cm.name) return false;


     Object.keys(selectedMasteries).forEach(function (name) {
     cmName = normalizeMasterClassName(cm.name);
        var cm = selectedMasteries[name];


        if (!selectedKey && isClassPathCm(cm)) {
    return masterClassTitles.some(function (title) {
            selectedKey = getClassPathKey(cm);
        return normalizeMasterClassName(title) === cmName;
        }
     });
     });
    return selectedKey;
}
}


function getSelectedExemplarPathChoice() {
    var selectedChoice = '';


    Object.keys(selectedMasteries).forEach(function (name) {
        var cm = selectedMasteries[name];


        if (!selectedChoice && isClassPathCm(cm)) {
            selectedChoice = stripRankSuffix(cm.name);
        }
    });


    return selectedChoice;
}


function getPreviousRankName(cm) {
    var rank = getRomanRank(cm && cm.name);
    var baseName = stripRankSuffix(cm && cm.name);
    var previousRank;


    if (rank <= 1) return '';


    previousRank = {
        2: 'I',
        3: 'II',
        4: 'III',
        5: 'IV'
    }[rank];


    return previousRank ? baseName + ' ' + previousRank : '';
}






function applySharedClassMasteriesFromBuilder(builderState) {
    var names;


    if (!builderState || sharedCmBuildApplied) return;


    names = safeArray(builderState.selectedClassMasteries);


    if (!names.length) return;


    selectedMasteries = {};


    names.forEach(function (name) {
var cm = findCmByNameOrId(name);


        if (cm) {
            selectedMasteries[cm.name] = cm;
        }
    });


function getExemplarPathKey(cm) {
     sharedCmBuildApplied = true;
     var flag = getExemplarFlag(cm);
}


    if (!flag) return '';


    return flag
        .toLowerCase()
        .replace(/\s+/g, '')
        .replace(/[_-]+/g, '')
        .replace(/rank[ivx0-9]+$/i, '')
        .replace(/[ivx]+$/i, '')
        .replace(/[0-9]+$/i, '');
}


function isExemplarCm(cm) {
         function getSelectedMasterClassTitle() {
    return !!getExemplarFlag(cm);
     var selectedTitle = '';
}
 
 
 
 
 
function isExemplarCm(cm) {
    var name = String((cm && cm.name) || '').toLowerCase();
    var classification = String((cm && cm.classification) || '').toLowerCase();
    var subCategory = String((cm && cm.subCategory) || '').toLowerCase();
    var searchText = [
        name,
        classification,
        subCategory,
         safeArray(cm && cm.requirements).join(' '),
        safeArray(cm && cm.spellsAdded).join(' '),
        safeArray(cm && cm.spellsRemoved).join(' ')
    ].join(' ').toLowerCase();
 
    return searchText.indexOf('exemplar') !== -1 ||
        classification === 'archetype' ||
        subCategory === 'archetype';
}
 
function getSelectedExemplarPathChoice() {
     var selectedExemplar = '';


     Object.keys(selectedMasteries).forEach(function (name) {
     Object.keys(selectedMasteries).forEach(function (name) {
         if (!selectedExemplar && isExemplarCm(selectedMasteries[name])) {
         if (isMasterClassTitle(selectedMasteries[name])) {
             selectedExemplar = name;
             selectedTitle = name;
         }
         }
     });
     });


     return selectedExemplar;
     return selectedTitle;
}
}


Line 793: Line 974:




function getSelectedExemplarPathKey() {
    var selectedKey = '';


    Object.keys(selectedMasteries).forEach(function (name) {
function getCmPlayerFlags(cm) {
        var key = getExemplarPathKey(selectedMasteries[name]);
    var flags = [];


        if (key && !selectedKey) {
    if (!cm) return flags;
            selectedKey = key;
        }
    });


     return selectedKey;
    safeArray(cm.playerFlags).forEach(function (flag) {
        flags.push(flag);
    });
 
    Object.keys(cm).forEach(function (key) {
        if (String(key).toLowerCase().indexOf('playerflag') !== -1) {
            if (typeof cm[key] === 'string') {
                flags.push(cm[key]);
            } else {
                safeArray(cm[key]).forEach(function (flag) {
                    flags.push(flag);
                });
            }
        }
    });
 
     return flags;
}
}


app.innerHTML =
function getRomanRank(value) {
    var match = String(value || '').trim().match(/\s+(I|II|III|IV|V)$/i);


            '<div class="cm-calculator-box">' +
    if (!match) return 0;
                '<div class="character-builder-title" style="display:none;">EQOA: Sandstorm Class Mastery Builder</div>' +


                '<div class="character-builder-controls">' +
    return {
                    '<label>Race</label>' +
        I: 1,
                    '<select id="cm-race"></select>' +
        II: 2,
        III: 3,
        IV: 4,
        V: 5
    }[match[1].toUpperCase()] || 0;
}
 
function stripRankSuffix(value) {
    return String(value || '')
        .trim()
        .replace(/\s+(I|II|III|IV|V)$/i, '');
}
 
function normalizePathKey(value) {
    return stripRankSuffix(value)
        .toLowerCase()
        .replace(/[^a-z0-9]/g, '');
}


                    '<label>Class</label>' +
                    '<select id="cm-class"></select>' +


                    '<label>Character Level</label>' +
                    '<input type="number" id="cm-level" min="1" max="60" value="60" style="width:70px;">' +


                    '<label>Base Stats</label>' +
                    '<span>' +
                        'STR <input type="number" id="cm-base-str" min="0" value="0" style="width:55px;"> ' +
                        'STA <input type="number" id="cm-base-sta" min="0" value="0" style="width:55px;"> ' +
                        'DEX <input type="number" id="cm-base-dex" min="0" value="0" style="width:55px;"> ' +
                        'AGI <input type="number" id="cm-base-agi" min="0" value="0" style="width:55px;"> ' +
                        'INT <input type="number" id="cm-base-int" min="0" value="0" style="width:55px;"> ' +
                        'WIS <input type="number" id="cm-base-wis" min="0" value="0" style="width:55px;"> ' +
                        'CHA <input type="number" id="cm-base-cha" min="0" value="0" style="width:55px;">' +
                    '</span>' +
       


                    '<button type="button" id="cm-reset" class="cm-reset-button">Reset</button>' +
                '</div>' +


                '<div class="character-builder-section-title" style="margin-top:15px;">Point Summary</div>' +
                '<table class="character-builder-stat-table">' +
                    '<tr><th>Available</th><th>Spent</th><th>Remaining</th></tr>' +
                    '<tr><td id="cm-available">1500</td><td id="cm-spent">0</td><td id="cm-remaining">1500</td></tr>' +
                '</table>' +


                '<div class="character-builder-section-title" style="margin-top:18px;">Master Class Title</div>' +
                '<div id="cm-master-class-panel" class="cm-master-class-panel">None selected</div>' +


                '<div class="cm-build-actions">' +
function getExemplarFlag(cm) {
                    '<button type="button" id="cm-share-url" class="cm-action-button">Generate Share URL</button>' +
    var found = '';
                    '<button type="button" id="cm-export-card" class="cm-action-button">Export Build Card</button>' +
    var questText = String((cm && cm.quest) || '').trim();
                '</div>' +


                '<div class="character-builder-section-title" style="margin-top:18px;">Stats / Spells Gained from CM</div>' +
    if (/^exemplar-/i.test(questText)) {
                '<div id="cm-unlocked-summary">No stats or spells gained yet.</div>' +
        return questText;
    }


                '<div class="character-builder-section-title" style="margin-top:18px;">Class Masteries</div>' +
    getCmPlayerFlags(cm).forEach(function (flag) {
                '<div class="cm-tabs">' +
        var text = String(flag || '').trim();
                    '<button type="button" class="cm-tab-button cm-tab-active" data-tab="General">General</button>' +
                    '<button type="button" class="cm-tab-button" data-tab="Racial">Racial</button>' +
                    '<button type="button" class="cm-tab-button" data-tab="Archetype">Archetype</button>' +
                    '<button type="button" class="cm-tab-button" data-tab="Class">Class</button>' +
                '</div>' +


                '<div class="cm-search-tools">' +
        if (!found && /^exemplar-/i.test(text)) {
                    '<div class="cm-search-row">' +
            found = text;
                        '<input type="search" id="cm-search" class="cm-search-input" placeholder="Search mastery, stat, spell, requirement...">' +
        }
                        '<button type="button" id="cm-clear-search" class="cm-clear-search-button">Clear</button>' +
    });
                    '</div>' +
                    '<label class="cm-lock-toggle"><input type="checkbox" id="cm-show-locked" checked> <span>Show locked</span></label>' +
                '</div>' +


                '<div class="cm-view-toggle">' +
    return found;
                    '<button type="button" class="cm-view-button cm-view-active" data-view="detailed">Detailed</button>' +
}
                    '<button type="button" class="cm-view-button" data-view="compact">Compact</button>' +
                '</div>' +


                '<div class="cm-main-layout">' +
function getPathFlag(cm) {
                    '<div class="cm-purchased-panel">' +
    return getExemplarFlag(cm);
                        '<div class="cm-panel-title">CM Purchased</div>' +
}
                        '<div id="cm-purchased-list">None purchased.</div>' +
                    '</div>' +
                    '<div id="cm-mastery-panel" class="character-builder-warning-box">Select a race and class to begin.</div>' +
                '</div>' +


                '<div id="cm-tooltip" class="cm-tooltip" style="display:none;"></div>' +
function isClassPathCm(cm) {
            '</div>';
    return !!getExemplarFlag(cm);
}


        var raceSelect = document.getElementById('cm-race');
function getClassPathKey(cm) {
        var classSelect = document.getElementById('cm-class');
    var flag = getExemplarFlag(cm);
        var levelInput = document.getElementById('cm-level');
 
        var pathSelect = document.getElementById('cm-path');
    if (flag) {
         var resetButton = document.getElementById('cm-reset');
         return normalizePathKey(flag);
        var searchInput = document.getElementById('cm-search');
    }
        var clearSearchButton = document.getElementById('cm-clear-search');
 
        var showLockedInput = document.getElementById('cm-show-locked');
    return normalizePathKey(cm && cm.name);
        var viewButtons = document.querySelectorAll('.cm-view-button');
}
        var cmTooltip = document.getElementById('cm-tooltip');
        var shareUrlButton = document.getElementById('cm-share-url');
        var exportCardButton = document.getElementById('cm-export-card');


        function applyCmTheme(theme) {
            app.classList.remove('cm-theme-dark');
            app.classList.remove('cm-theme-light');
            app.classList.add(theme === 'light' ? 'cm-theme-light' : 'cm-theme-dark');
        }


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


            try {
                var saved = localStorage.getItem('eqoa-wiki-theme');
                if (saved === 'light') isLight = true;
                if (saved === 'dark') isLight = false;
            } catch (e) {}


            applyCmTheme(isLight ? 'light' : 'dark');
        }


        function loadRaces() {
            raceSelect.innerHTML = '';


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


            loadClasses();
        }


        function loadClasses() {
function getSelectedClassPathKey() {
            var race = raceSelect.value;
    var selectedKey = '';
            var classList = safeArray((data.raceClasses || {})[race]);


            classSelect.innerHTML = '';
    Object.keys(selectedMasteries).forEach(function (name) {
        var cm = selectedMasteries[name];


            classList.forEach(function (classCode) {
        if (!selectedKey && isClassPathCm(cm)) {
                addOption(
             selectedKey = getClassPathKey(cm);
                    classSelect,
                    classCode,
                    classCode + ' - ' + ((data.classes || {})[classCode] || classCode)
                );
            });
 
             updateSummary();
         }
         }
    });


        function syncFromCharacterBuilder() {
    return selectedKey;
            var builderState = getBuilderState();
}


            if (!builderState) return;
function getSelectedExemplarPathChoice() {
    var selectedChoice = '';


            if (builderState.race && data.raceClasses[builderState.race]) {
    Object.keys(selectedMasteries).forEach(function (name) {
                raceSelect.value = builderState.race;
        var cm = selectedMasteries[name];
                loadClasses();
            }


            if (builderState.classCode) {
        if (!selectedChoice && isClassPathCm(cm)) {
                classSelect.value = builderState.classCode;
            selectedChoice = stripRankSuffix(cm.name);
            }
        }
    });


            if (builderState.level !== undefined) {
    return selectedChoice;
                levelInput.value = builderState.level;
}
            }


            if (builderState.baseStats) {
function getPreviousRankName(cm) {
                updateBaseStatInputsDisplay(builderState.baseStats);
    var rank = getRomanRank(cm && cm.name);
            }
    var baseName = stripRankSuffix(cm && cm.name);
    var previousRank;


            updateSummary();
    if (rank <= 1) return '';
        }


         window.EQOA_SYNC_CLASS_MASTERY_FROM_BUILDER = syncFromCharacterBuilder;
    previousRank = {
         2: 'I',
        3: 'II',
        4: 'III',
        5: 'IV'
    }[rank];


        if (isEmbedded()) {
    return previousRank ? baseName + ' ' + previousRank : '';
            raceSelect.disabled = true;
}
            classSelect.disabled = true;
 
            levelInput.disabled = true;
function getClassPathLockReasons(cm) {
        }
    var reasons = [];
    var selectedPathKey;
    var thisPathKey;
    var previousRankName;


        function getBuildTotals() {
    if (!isClassPathCm(cm)) return reasons;
            var statTotals = {};
            var spellsAdded = {};
            var spellsRemoved = {};


            Object.keys(selectedMasteries).forEach(function (name) {
    selectedPathKey = getSelectedClassPathKey();
                var cm = selectedMasteries[name];
    thisPathKey = getClassPathKey(cm);
    previousRankName = getPreviousRankName(cm);
 
    if (selectedPathKey && thisPathKey && selectedPathKey !== thisPathKey) {
        reasons.push('You already selected a different Class / Exemplar Path');
    }
 
    if (previousRankName && !selectedMasteries[previousRankName]) {
        reasons.push('Requires ' + previousRankName + ' first');
    }
 
    return reasons;
}


                Object.keys(cm.stats || {}).forEach(function (statName) {
                    var value = parseInt(cm.stats[statName], 10);


                    if (!isNaN(value) && value !== 0) {
                        statTotals[statName] = (statTotals[statName] || 0) + value;
                    }
                });


                safeArray(cm.spellsAdded).forEach(function (spell) {
                    if (spell) spellsAdded[spell] = true;
                });


                safeArray(cm.spellsRemoved).forEach(function (spell) {
                    if (spell) spellsRemoved[spell] = true;
                });
            });


            return {
                stats: statTotals,
                spellsAdded: spellsAdded,
                spellsRemoved: spellsRemoved
            };
        }


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


    Object.keys(selectedMasteries).forEach(function (name) {
        var stats = selectedMasteries[name].stats || {};


        caps.str += numberOrZero(stats.strMax);
app.innerHTML =
        caps.sta += numberOrZero(stats.staMax);
        caps.agi += numberOrZero(stats.agiMax);
        caps.dex += numberOrZero(stats.dexMax);
        caps.wis += numberOrZero(stats.wisMax);
        caps.int += numberOrZero(stats.intMax);
        caps.cha += numberOrZero(stats.chaMax);
    });


    return caps;
            '<div class="cm-calculator-box">' +
}
                '<div class="character-builder-title" style="display:none;">EQOA: Sandstorm Class Mastery Builder</div>' +


function getSelectedMasteryByType(typeName) {
                '<div class="character-builder-controls">' +
    var found = '';
                    '<label>Race</label>' +
                    '<select id="cm-race"></select>' +


    Object.keys(selectedMasteries).sort().forEach(function (name) {
                    '<label>Class</label>' +
        var cm = selectedMasteries[name];
                    '<select id="cm-class"></select>' +
        var type = getCmType(cm);


        if (!found && type === typeName) {
                    '<label>Character Level</label>' +
            found = name;
                    '<input type="number" id="cm-level" min="1" max="60" value="60" style="width:70px;">' +
        }
    });


     return found;
                    '<label>Base Stats</label>' +
}
                    '<span>' +
                        'STR <input type="number" id="cm-base-str" min="0" value="0" style="width:55px;"> ' +
                        'STA <input type="number" id="cm-base-sta" min="0" value="0" style="width:55px;"> ' +
                        'DEX <input type="number" id="cm-base-dex" min="0" value="0" style="width:55px;"> ' +
                        'AGI <input type="number" id="cm-base-agi" min="0" value="0" style="width:55px;"> ' +
                        'INT <input type="number" id="cm-base-int" min="0" value="0" style="width:55px;"> ' +
                        'WIS <input type="number" id="cm-base-wis" min="0" value="0" style="width:55px;"> ' +
                        'CHA <input type="number" id="cm-base-cha" min="0" value="0" style="width:55px;">' +
                    '</span>' +
       
 
                    '<button type="button" id="cm-reset" class="cm-reset-button">Reset</button>' +
                '</div>' +
 
              '<div class="character-builder-section-title" style="margin-top:15px;">Point Summary</div>' +
'<table class="character-builder-stat-table">' +
    '<tr><th>Available</th><th>Spent</th><th>Remaining</th></tr>' +
     '<tr><td id="cm-available">1500</td><td id="cm-spent">0</td><td id="cm-remaining">1500</td></tr>' +
'</table>' +
 
'<div class="character-builder-section-title cm-master-class-heading" style="margin-top:18px;">Master Class Title</div>' +
'<div id="cm-master-class-panel" class="cm-master-class-panel"><span class="cm-master-class-text">-</span></div>' +


function getClassMasteryExportData() {
'<div class="character-builder-section-title" style="margin-top:18px;">Stats / Spells Gained from CM</div>' +
    var totals = getBuildTotals();
                '<div id="cm-unlocked-summary">No stats or spells gained yet.</div>' +


    return {
                '<div class="character-builder-section-title" style="margin-top:18px;">Class Masteries</div>' +
        race: raceSelect ? raceSelect.value : '',
                '<div class="cm-tabs">' +
        classCode: classSelect ? classSelect.value : '',
                    '<button type="button" class="cm-tab-button cm-tab-active" data-tab="General">General</button>' +
        level: levelInput ? levelInput.value : '',
                    '<button type="button" class="cm-tab-button" data-tab="Racial">Racial</button>' +
        path: getSelectedTransformationPath(),
                    '<button type="button" class="cm-tab-button" data-tab="Archetype">Archetype</button>' +
        classPathChoice: getSelectedMasteryByType('Class'),
                    '<button type="button" class="cm-tab-button" data-tab="Class">Class</button>' +
        exemplarPathChoice: getSelectedExemplarPathChoice(),
                '</div>' +
        available: getAvailablePoints(),
        spent: getSpentPoints(),
        remaining: Math.max(0, getAvailablePoints() - getSpentPoints()),
        masterClassTitle: getSelectedMasterClassTitle(),
        selectedMasteries: Object.keys(selectedMasteries).sort(),
        stats: totals.stats,
        spellsAdded: Object.keys(totals.spellsAdded).sort(),
        spellsRemoved: Object.keys(totals.spellsRemoved).sort()
    };
}


function publishClassMasteryState() {
                '<div class="cm-search-tools">' +
    window.EQOA_CLASS_MASTERY_STATE = {
                    '<div class="cm-search-row">' +
        getStatCaps: getUnlockedStatCaps,
                        '<input type="search" id="cm-search" class="cm-search-input" placeholder="Search mastery, stat, spell, requirement...">' +
        getSelectedMasteries: function () {
                        '<button type="button" id="cm-clear-search" class="cm-clear-search-button">Clear</button>' +
            return Object.keys(selectedMasteries).sort();
                    '</div>' +
        },
                    '<label class="cm-lock-toggle"><input type="checkbox" id="cm-show-locked" checked> <span>Show locked</span></label>' +
        getExportData: getClassMasteryExportData
                '</div>' +
    };


    if (window.dispatchEvent) {
                '<div class="cm-view-toggle">' +
        window.dispatchEvent(new CustomEvent('eqoa-class-mastery-state-change'));
                    '<button type="button" class="cm-view-button cm-view-active" data-view="detailed">Detailed</button>' +
    }
                    '<button type="button" class="cm-view-button" data-view="compact">Compact</button>' +
}
                '</div>' +


         function updatePurchasedList() {
              '<div class="cm-main-layout">' +
            var list = document.getElementById('cm-purchased-list');
    '<div class="cm-purchased-panel">' +
            var names = Object.keys(selectedMasteries).sort();
         '<div class="cm-panel-title">Master Class</div>' +
        '<div id="cm-master-class-purchased-list">None selected.</div>' +
        '<div class="cm-panel-title" style="margin-top:12px;">CM Purchased</div>' +
        '<div id="cm-purchased-list">None purchased.</div>' +
    '</div>' +
    '<div id="cm-mastery-panel" class="character-builder-warning-box">Select a race and class to begin.</div>' +
'</div>' +


             if (!list) return;
                '<div id="cm-tooltip" class="cm-tooltip" style="display:none;"></div>' +
             '</div>';


            if (!names.length) {
        var raceSelect = document.getElementById('cm-race');
                list.innerHTML = 'None purchased.';
        var classSelect = document.getElementById('cm-class');
                return;
        var levelInput = document.getElementById('cm-level');
            }
        var pathSelect = document.getElementById('cm-path');
        var resetButton = document.getElementById('cm-reset');
        var searchInput = document.getElementById('cm-search');
        var clearSearchButton = document.getElementById('cm-clear-search');
        var showLockedInput = document.getElementById('cm-show-locked');
        var viewButtons = document.querySelectorAll('.cm-view-button');
        var cmTooltip = document.getElementById('cm-tooltip');


            list.innerHTML = names.map(function (name) {
                var cm = selectedMasteries[name];


                return '<div class="cm-purchased-item" data-cm-name="' + escapeHtml(name) + '">' +
        function applyCmTheme(theme) {
                    '<span>' + escapeHtml(name) + '</span>' +
            app.classList.remove('cm-theme-dark');
                    '<span class="cm-purchased-cost">' + escapeHtml(cm.points) + '</span>' +
             app.classList.remove('cm-theme-light');
                '</div>';
             app.classList.add(theme === 'light' ? 'cm-theme-light' : 'cm-theme-dark');
             }).join('');
 
             Array.prototype.forEach.call(list.querySelectorAll('.cm-purchased-item'), function (row) {
                row.addEventListener('click', function () {
                    delete selectedMasteries[row.getAttribute('data-cm-name')];
                    updateSummary();
                });
 
            });
         }
         }


         function updateMasterClassPanel() {
         function loadSavedCmTheme() {
             var panel = document.getElementById('cm-master-class-panel');
             var isLight =
            var selectedTitle = getSelectedMasterClassTitle();
                document.documentElement.classList.contains('eqoa-theme-light') ||
                document.body.classList.contains('eqoa-theme-light');


             if (!panel) return;
             try {
                var saved = localStorage.getItem('eqoa-wiki-theme');
                if (saved === 'light') isLight = true;
                if (saved === 'dark') isLight = false;
            } catch (e) {}


             panel.innerHTML = selectedTitle ? escapeHtml(selectedTitle) : 'None selected';
             applyCmTheme(isLight ? 'light' : 'dark');
         }
         }


         function updateUnlockedSummary() {
         function loadRaces() {
             var summary = document.getElementById('cm-unlocked-summary');
             raceSelect.innerHTML = '';
            var totals = getBuildTotals();
            var html = '<div class="cm-unlocked-grid cm-unlocked-grid-three">';


             html += '<div class="cm-unlocked-box"><div class="cm-panel-title">Stats</div>';
             Object.keys(data.raceClasses || {}).forEach(function (race) {
            if (!Object.keys(totals.stats).length) {
                 addOption(raceSelect, race, race);
                html += '<div>No stat changes yet.</div>';
            } else {
                Object.keys(totals.stats).sort().forEach(function (statName) {
                    var value = totals.stats[statName];
                    html += '<div class="cm-purchased-item"><span>' +
                        escapeHtml(formatStatName(statName)) +
                        '</span><span class="cm-purchased-cost">' +
                        (value > 0 ? '+' : '') +
                        escapeHtml(value) +
                        '</span></div>';
                });
            }
            html += '</div>';
 
            html += '<div class="cm-unlocked-box"><div class="cm-panel-title">Spells Gained</div>';
            Object.keys(totals.spellsAdded).sort().forEach(function (spell) {
                 html += '<div class="cm-purchased-item">+ ' + escapeHtml(spell) + '</div>';
             });
             });
            if (!Object.keys(totals.spellsAdded).length) html += '<div>No spells gained yet.</div>';
            html += '</div>';


             html += '<div class="cm-unlocked-box"><div class="cm-panel-title">Spells Removed</div>';
             loadClasses();
            Object.keys(totals.spellsRemoved).sort().forEach(function (spell) {
                html += '<div class="cm-purchased-item">- ' + escapeHtml(spell) + '</div>';
            });
            if (!Object.keys(totals.spellsRemoved).length) html += '<div>No spells removed yet.</div>';
            html += '</div>';
 
            html += '</div>';
 
            summary.innerHTML = html;
         }
         }


         function updateSummary() {
         function loadClasses() {
             var available = getAvailablePoints();
             var race = raceSelect.value;
             var spent = getSpentPoints();
             var classList = safeArray((data.raceClasses || {})[race]);
            var remaining = available - spent;


             if (remaining < 0) remaining = 0;
             classSelect.innerHTML = '';


             if (isEmbedded()) {
             classList.forEach(function (classCode) {
                 updateBaseStatInputsDisplay(getBaseStatsFromInputs());
                 addOption(
             }
                    classSelect,
                    classCode,
                    classCode + ' - ' + ((data.classes || {})[classCode] || classCode)
                );
             });


             document.getElementById('cm-available').textContent = available;
             updateSummary();
            document.getElementById('cm-spent').textContent = spent;
            document.getElementById('cm-remaining').textContent = remaining;
 
            updatePurchasedList();
updateMasterClassPanel();
updateUnlockedSummary();
publishClassMasteryState();
renderMasteryList();
         }
         }


         function buildTooltipHtml(cm) {
         function syncFromCharacterBuilder() {
             var html = '';
             var builderState = getBuilderState();


             html += '<div class="cm-tooltip-title">' + escapeHtml(cm.name) + '</div>';
             if (!builderState) return;
            html += '<div class="cm-tooltip-desc">' + escapeHtml(cm.description) + '</div>';
            html += '<div class="cm-tooltip-meta">Level ' + escapeHtml(cm.level) +
                ' | Cost ' + escapeHtml(cm.points) +
                (cm.cmSpentReq ? ' | CM Spent Req ' + escapeHtml(cm.cmSpentReq) : '') +
                '</div>';
            html += '<div class="cm-tooltip-meta">' + escapeHtml(getCmPointStatusText()) + '</div>';


             Object.keys(cm.stats || {}).forEach(function (key) {
             if (builderState.race && data.raceClasses[builderState.race]) {
                 var value = cm.stats[key];
                 raceSelect.value = builderState.race;
                 var sign = value > 0 ? '+' : '';
                 loadClasses();
            }


                html += '<div class="cm-tooltip-stat">' +
            if (builderState.classCode) {
                    escapeHtml(formatStatName(key)) +
                classSelect.value = builderState.classCode;
                    ': ' +
             }
                    sign +
                    escapeHtml(value) +
                    '</div>';
             });


             safeArray(cm.spellsAdded).forEach(function (spell) {
             if (builderState.level !== undefined) {
                 html += '<div class="cm-tooltip-spell-add">+ ' + escapeHtml(spell) + '</div>';
                 levelInput.value = builderState.level;
             });
             }


             safeArray(cm.spellsRemoved).forEach(function (spell) {
             if (builderState.baseStats) {
                html += '<div class="cm-tooltip-spell-remove">- ' + escapeHtml(spell) + '</div>';
    updateBaseStatInputsDisplay(builderState.baseStats);
            });
}
 
applySharedClassMasteriesFromBuilder(builderState);


            return html;
updateSummary();
         }
         }


         function showCmTooltip(cm, card) {
         window.EQOA_SYNC_CLASS_MASTERY_FROM_BUILDER = syncFromCharacterBuilder;
            var rect;
            var tooltipWidth = 340;
            var leftPos;


            if (!cmTooltip || !cm) return;
        if (isEmbedded()) {
            raceSelect.disabled = true;
            classSelect.disabled = true;
            levelInput.disabled = true;
        }


             cmTooltip.innerHTML = buildTooltipHtml(cm);
        function getBuildTotals() {
             cmTooltip.style.display = 'block';
             var statTotals = {};
            var spellsAdded = {};
             var spellsRemoved = {};


             rect = card.getBoundingClientRect();
             Object.keys(selectedMasteries).forEach(function (name) {
            leftPos = rect.right + 12;
                var cm = selectedMasteries[name];


            if (leftPos + tooltipWidth > window.innerWidth) {
                Object.keys(cm.stats || {}).forEach(function (statName) {
                leftPos = rect.left - tooltipWidth - 12;
                    var value = parseInt(cm.stats[statName], 10);
            }
 
                    if (!isNaN(value) && value !== 0) {
                        statTotals[statName] = (statTotals[statName] || 0) + value;
                    }
                });
 
                safeArray(cm.spellsAdded).forEach(function (spell) {
                    if (spell) spellsAdded[spell] = true;
                });


            cmTooltip.style.left = leftPos + 'px';
                safeArray(cm.spellsRemoved).forEach(function (spell) {
            cmTooltip.style.top = Math.max(10, rect.top) + 'px';
                    if (spell) spellsRemoved[spell] = true;
        }
                });
            });


        function hideCmTooltip() {
            return {
             if (cmTooltip) cmTooltip.style.display = 'none';
                stats: statTotals,
                spellsAdded: spellsAdded,
                spellsRemoved: spellsRemoved
             };
         }
         }


        function renderMasteryList() {
function getUnlockedStatCaps() {
            var masteryPanel = document.getElementById('cm-mastery-panel');
    var caps = {
            var selectedRace = raceSelect.value;
        str: 400,
            var selectedRaceCode = getRaceCode(selectedRace);
        sta: 400,
            var selectedClass = classSelect.value;
        agi: 400,
            var selectedPathNormalized = normalizePath(getSelectedTransformationPath());
        dex: 400,
            var selectedIsWere = isWerePath(selectedPathNormalized);
        wis: 400,
            var searchQuery = searchInput ? String(searchInput.value || '').toLowerCase() : '';
        int: 400,
            var seen = {};
        cha: 400
            var availableMasteries = [];
    };
            var lockedMasteries = [];


            if (!masteryPanel) return;
    Object.keys(selectedMasteries).forEach(function (name) {
        var stats = selectedMasteries[name].stats || {};


            visibleMasteries = [];
        caps.str += numberOrZero(stats.strMax);
        caps.sta += numberOrZero(stats.staMax);
        caps.agi += numberOrZero(stats.agiMax);
        caps.dex += numberOrZero(stats.dexMax);
        caps.wis += numberOrZero(stats.wisMax);
        caps.int += numberOrZero(stats.intMax);
        caps.cha += numberOrZero(stats.chaMax);
    });


            safeArray(data.masteries).forEach(function (cm) {
    return caps;
                var cmName = String(cm.name || '');
}
                var cmPath = normalizePath(cm.lycanthropy);
                var bitePath = getBitePath(cmName);
                var pathOk = false;
                var raceOk;
                var classOk;
                var tabOk;
                var searchOk;
                var lockReasons;


                raceOk = listAllowsValue(cm.races, selectedRaceCode);
function getSelectedMasteryByType(typeName) {
                classOk = listAllowsValue(cm.classes, selectedClass);
    var found = '';
                tabOk = String(getCmDisplayTab(cm)).toLowerCase() === String(activeCmTab).toLowerCase();
                searchOk = !searchQuery || getCmSearchText(cm).indexOf(searchQuery) !== -1;


                if (bitePath) {
    Object.keys(selectedMasteries).sort().forEach(function (name) {
                    pathOk = selectedPathNormalized === bitePath;
        var cm = selectedMasteries[name];
                } else if (!cmPath) {
        var type = getCmType(cm);
                    pathOk = true;
 
                } else if (!selectedPathNormalized) {
        if (!found && type === typeName) {
                    pathOk = false;
            found = name;
                } else if (selectedIsWere && isSharedWereMastery(cmName)) {
        }
                    pathOk = cmPath === 'werebeast' || isWerePath(cmPath);
    });
                } else if (selectedIsWere && cmPath === 'werebeast') {
                    pathOk = true;
                } else {
                    pathOk = cmPath === selectedPathNormalized;
                }


                if (
    return found;
                    !raceOk ||
}
                    !classOk ||
                    !tabOk ||
                    !pathOk ||
                    !searchOk ||
                    selectedMasteries[cmName] ||
                    seen[cmName]
                ) {
                    return;
                }


                seen[cmName] = true;
function getClassMasteryExportData() {
    var totals = getBuildTotals();


                lockReasons = getCmLockReasons(cm, false);
    return {
        race: raceSelect ? raceSelect.value : '',
        classCode: classSelect ? classSelect.value : '',
        level: levelInput ? levelInput.value : '',
        path: getSelectedTransformationPath(),
        classPathChoice: getSelectedMasteryByType('Class'),
        exemplarPathChoice: getSelectedExemplarPathChoice(),
        available: getAvailablePoints(),
        spent: getSpentPoints(),
        remaining: Math.max(0, getAvailablePoints() - getSpentPoints()),
        masterClassTitle: getSelectedMasterClassTitle(),
selectedMasteries: Object.keys(selectedMasteries).sort(),
selectedMasteryIds: Object.keys(selectedMasteries).map(function (name) {
    var cm = selectedMasteries[name];


                if (lockReasons.length) {
    return cm.id || cm.cmId || cm.CmID || cm.name;
                    lockedMasteries.push({
}).sort(),
                        cm: cm,
        stats: totals.stats,
                        locked: true,
        spellsAdded: Object.keys(totals.spellsAdded).sort(),
                        unlockHint: lockReasons.join(' | ')
        spellsRemoved: Object.keys(totals.spellsRemoved).sort()
                    });
    };
                } else {
}
                    availableMasteries.push({
 
                        cm: cm,
function publishClassMasteryState() {
                        locked: false,
    window.EQOA_CLASS_MASTERY_STATE = {
                        unlockHint: ''
        getStatCaps: getUnlockedStatCaps,
                    });
getSelectedMasteries: function () {
                }
    return Object.keys(selectedMasteries).map(function (name) {
            });
        var cm = selectedMasteries[name];


            visibleMasteries = showLockedCms ? availableMasteries.concat(lockedMasteries) : availableMasteries;
        return cm.id || cm.cmId || cm.CmID || cm.name;
    }).sort();
},
        getExportData: getClassMasteryExportData
    };


            if (!visibleMasteries.length) {
    if (window.dispatchEvent) {
                masteryPanel.innerHTML =
        window.dispatchEvent(new CustomEvent('eqoa-class-mastery-state-change'));
                    'No Class Masteries found for this selection.<br>' +
    }
                    '<small>Loaded masteries: ' + safeArray(data.masteries).length + '</small>';
}
                return;
 
            }
function updatePurchasedList() {
    var list = document.getElementById('cm-purchased-list');
    var masterList = document.getElementById('cm-master-class-purchased-list');
    var names = Object.keys(selectedMasteries).sort();
    var masterNames = [];
    var purchasedNames = [];
 
    names.forEach(function (name) {
        if (isMasterClassTitle(selectedMasteries[name])) {
            masterNames.push(name);
        } else {
            purchasedNames.push(name);
        }
    });


            masteryPanel.innerHTML = visibleMasteries.map(function (entry, index) {
    function renderRows(container, rowNames, emptyText) {
                var cm = entry.cm;
        if (!container) return;
                var lockedClass = entry.locked ? ' cm-card-locked' : '';
                var typeClass = getCmTypeClass(cm);
                var cmType = getCmType(cm);
                var reqHtml = getRequirementsHtml(cm);
                var statReqHtml = getStatReqHtml(cm);
                var lockHtml = '';
                var metaHtml;


                if (entry.locked) {
        if (!rowNames.length) {
                    lockHtml =
            container.innerHTML = emptyText;
                        '<div class="cm-unlock-hint">' +
            return;
                            '<strong>Locked:</strong><br>' +
        }
                            escapeHtml(entry.unlockHint).replace(/\|/g, '<br>') +
                        '</div>';
                }


                metaHtml =
        container.innerHTML = rowNames.map(function (name) {
                    '<div class="cm-card-meta">' +
            var cm = selectedMasteries[name];
                        'Level: ' + escapeHtml(cm.level) +
                        ' | Cost: ' + escapeHtml(cm.points) +
                        (cm.cmSpentReq ? ' | CM Spent Req: ' + escapeHtml(cm.cmSpentReq) : '') +
                    '</div>' +
                    '<div class="cm-card-meta">' + escapeHtml(getCmPointStatusText()) + '</div>';


                if (activeCmView === 'compact') {
            return '<div class="cm-purchased-item" data-cm-name="' + escapeHtml(name) + '">' +
                    return '<div class="cm-card cm-purchasable-card cm-card-compact ' + typeClass + lockedClass + '" data-cm-index="' + index + '">' +
                '<span>' + escapeHtml(name) + '</span>' +
                        '<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
                '<span class="cm-purchased-cost">' + escapeHtml(cm.points) + '</span>' +
                        '<div class="cm-card-type-tag">' + escapeHtml(cmType) + '</div>' +
            '</div>';
                        metaHtml +
        }).join('');
                        (statReqHtml ? '<div class="cm-card-req">Stat Req: ' + statReqHtml + '</div>' : '') +
                        lockHtml +
                    '</div>';
                }


                return '<div class="cm-card cm-purchasable-card ' + typeClass + lockedClass + '" data-cm-index="' + index + '">' +
        Array.prototype.forEach.call(container.querySelectorAll('.cm-purchased-item'), function (row) {
                    '<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
            var cmName = row.getAttribute('data-cm-name');
                    '<div class="cm-card-type-tag">' + escapeHtml(cmType) + '</div>' +
             var cm = selectedMasteries[cmName];
                    '<div class="cm-card-desc">' + escapeHtml(cm.description) + '</div>' +
                    metaHtml +
                    (reqHtml ? '<div class="cm-card-req">Requires: ' + reqHtml + '</div>' : '') +
                    (statReqHtml ? '<div class="cm-card-req">Stat Req: ' + statReqHtml + '</div>' : '') +
                    lockHtml +
                '</div>';
             }).join('');


             Array.prototype.forEach.call(masteryPanel.querySelectorAll('.cm-purchasable-card'), function (card) {
             row.addEventListener('mouseenter', function () {
                 var index = parseInt(card.getAttribute('data-cm-index'), 10);
                 showCmTooltip(cm, row);
                var entry = visibleMasteries[index];
            });
                var cm = entry ? entry.cm : null;


                card.addEventListener('mouseenter', function () {
            row.addEventListener('mouseleave', hideCmTooltip);
                    showCmTooltip(cm, card);
                });


                card.addEventListener('mouseleave', hideCmTooltip);
            row.addEventListener('click', function () {
                delete selectedMasteries[cmName];
                hideCmTooltip();
                updateSummary();
            });
        });
    }


                card.addEventListener('click', function () {
    renderRows(masterList, masterNames, 'None selected.');
                    var reasons;
    renderRows(list, purchasedNames, 'None purchased.');
                    var selectedTitle;
}


                    if (!cm || !entry) return;
function updateMasterClassPanel() {
    var panel = document.getElementById('cm-master-class-panel');
    var selectedTitle = getSelectedMasterClassTitle();


                    if (entry.locked) {
    if (!panel) return;
                        alert(entry.unlockHint);
                        return;
                    }


                    reasons = getCmLockReasons(cm, true);
    panel.innerHTML =
        '<span class="cm-master-class-text">' +
        (selectedTitle ? escapeHtml(selectedTitle) : '-') +
        '</span>';
}


                    if (reasons.length) {
        function updateUnlockedSummary() {
                        alert(reasons.join(' | '));
            var summary = document.getElementById('cm-unlocked-summary');
                        return;
            var totals = getBuildTotals();
                    }
            var html = '<div class="cm-unlocked-grid cm-unlocked-grid-three">';


                    if (isMasterClassTitle(cm)) {
            html += '<div class="cm-unlocked-box"><div class="cm-panel-title">Stats</div>';
    selectedTitle = getSelectedMasterClassTitle();
            if (!Object.keys(totals.stats).length) {
                html += '<div>No stat changes yet.</div>';
            } else {
                Object.keys(totals.stats).sort().forEach(function (statName) {
                    var value = totals.stats[statName];
                    html += '<div class="cm-purchased-item"><span>' +
                        escapeHtml(formatStatName(statName)) +
                        '</span><span class="cm-purchased-cost">' +
                        (value > 0 ? '+' : '') +
                        escapeHtml(value) +
                        '</span></div>';
                });
            }
            html += '</div>';


    if (selectedTitle && selectedTitle !== cm.name) {
            html += '<div class="cm-unlocked-box"><div class="cm-panel-title">Spells Gained</div>';
        alert('You can only select one Master Class Title per character build.');
            Object.keys(totals.spellsAdded).sort().forEach(function (spell) {
        return;
                html += '<div class="cm-purchased-item">+ ' + escapeHtml(spell) + '</div>';
    }
            });
}
            if (!Object.keys(totals.spellsAdded).length) html += '<div>No spells gained yet.</div>';
            html += '</div>';


            html += '<div class="cm-unlocked-box"><div class="cm-panel-title">Spells Removed</div>';
            Object.keys(totals.spellsRemoved).sort().forEach(function (spell) {
                html += '<div class="cm-purchased-item">- ' + escapeHtml(spell) + '</div>';
            });
            if (!Object.keys(totals.spellsRemoved).length) html += '<div>No spells removed yet.</div>';
            html += '</div>';


if (isClassPathCm(cm)) {
            html += '</div>';
    var selectedPathKey = getSelectedClassPathKey();
    var thisPathKey = getClassPathKey(cm);
    var previousRankName = getPreviousRankName(cm);


    if (
            summary.innerHTML = html;
        selectedPathKey &&
         }
        thisPathKey &&
        selectedPathKey !== thisPathKey
    ) {
        alert('You can only select one Class / Exemplar Path per character build.');
         return;
    }


    if (previousRankName && !selectedMasteries[previousRankName]) {
        function updateSummary() {
        alert('Requires ' + previousRankName + ' first.');
            var available = getAvailablePoints();
        return;
            var spent = getSpentPoints();
    }
            var remaining = available - spent;
}


            if (remaining < 0) remaining = 0;


selectedMasteries[cm.name] = cm;
            if (isEmbedded()) {
                    hideCmTooltip();
                updateBaseStatInputsDisplay(getBaseStatsFromInputs());
                    updateSummary();
             }
                });
             });
        }


        function buildShareUrl() {
             document.getElementById('cm-available').textContent = available;
             return window.location.href.split('#')[0] +
            document.getElementById('cm-spent').textContent = spent;
                '#cm-build=' +
            document.getElementById('cm-remaining').textContent = remaining;
                encodeURIComponent(JSON.stringify({
                    r: raceSelect.value,
                    c: classSelect.value,
                    l: levelInput.value,
                    p: getSelectedTransformationPath(),
                    b: getBaseStatsFromInputs(),
                    cms: Object.keys(selectedMasteries).sort()
                }));
        }


        function copyTextToClipboard(text, button) {
             updatePurchasedList();
             if (navigator.clipboard && navigator.clipboard.writeText) {
updateMasterClassPanel();
                navigator.clipboard.writeText(text).then(function () {
updateUnlockedSummary();
                    var oldText = button.textContent;
publishClassMasteryState();
                    button.textContent = 'Copied';
renderMasteryList();
                    setTimeout(function () {
                        button.textContent = oldText;
                    }, 1200);
                });
            } else {
                alert(text);
            }
         }
         }


      function renderBuildCard() {
        function buildTooltipHtml(cm) {
    if (window.EQOA_EXPORT_COMBINED_BUILD_CARD) {
            var html = '';
         window.EQOA_EXPORT_COMBINED_BUILD_CARD();
 
         return;
            html += '<div class="cm-tooltip-title">' + escapeHtml(cm.name) + '</div>';
     }
            html += '<div class="cm-tooltip-desc">' + escapeHtml(cm.description) + '</div>';
 
            html += '<div class="cm-tooltip-meta">Level ' + escapeHtml(cm.level) +
     alert('Open the Character Builder page to export the combined Character Builder + Class Mastery card.');
                ' | Cost ' + escapeHtml(cm.points) +
}
                (cm.cmSpentReq ? ' | CM Spent Req ' + escapeHtml(cm.cmSpentReq) : '') +
          
                '</div>';
 
            html += '<div class="cm-tooltip-meta">' + escapeHtml(getCmPointStatusText()) + '</div>';
         function setBaseStatInputs(baseStats) {
 
             baseStats = baseStats || {};
            Object.keys(cm.stats || {}).forEach(function (key) {
             document.getElementById('cm-base-str').value = numberOrZero(baseStats.STR);
                var value = cm.stats[key];
             document.getElementById('cm-base-sta').value = numberOrZero(baseStats.STA);
                var sign = value > 0 ? '+' : '';
             document.getElementById('cm-base-dex').value = numberOrZero(baseStats.DEX);
 
             document.getElementById('cm-base-agi').value = numberOrZero(baseStats.AGI);
                html += '<div class="cm-tooltip-stat">' +
             document.getElementById('cm-base-int').value = numberOrZero(baseStats.INT);
                    escapeHtml(formatStatName(key)) +
             document.getElementById('cm-base-wis').value = numberOrZero(baseStats.WIS);
                    ': ' +
             document.getElementById('cm-base-cha').value = numberOrZero(baseStats.CHA);
                    sign +
         }
                    escapeHtml(value) +
 
                    '</div>';
         function loadBuildFromUrl() {
            });
             var hash = window.location.hash || '';
 
             var build;
            safeArray(cm.spellsAdded).forEach(function (spell) {
 
                html += '<div class="cm-tooltip-spell-add">+ ' + escapeHtml(spell) + '</div>';
             if (hash.indexOf('#cm-build=') !== 0) return;
            });
 
            safeArray(cm.spellsRemoved).forEach(function (spell) {
                html += '<div class="cm-tooltip-spell-remove">- ' + escapeHtml(spell) + '</div>';
            });
 
            return html;
        }
 
        function showCmTooltip(cm, card) {
            var rect;
            var tooltipWidth = 340;
            var leftPos;
 
            if (!cmTooltip || !cm) return;
 
            cmTooltip.innerHTML = buildTooltipHtml(cm);
            cmTooltip.style.display = 'block';
 
            rect = card.getBoundingClientRect();
            leftPos = rect.right + 12;
 
            if (leftPos + tooltipWidth > window.innerWidth) {
                leftPos = rect.left - tooltipWidth - 12;
            }
 
            cmTooltip.style.left = leftPos + 'px';
            cmTooltip.style.top = Math.max(10, rect.top) + 'px';
         }
 
        function hideCmTooltip() {
            if (cmTooltip) cmTooltip.style.display = 'none';
        }
 
        function renderMasteryList() {
            var masteryPanel = document.getElementById('cm-mastery-panel');
            var selectedRace = raceSelect.value;
            var selectedRaceCode = getRaceCode(selectedRace);
            var selectedClass = classSelect.value;
            var selectedPathNormalized = normalizePath(getSelectedTransformationPath());
            var selectedIsWere = isWerePath(selectedPathNormalized);
            var searchQuery = searchInput ? String(searchInput.value || '').toLowerCase() : '';
            var seen = {};
            var availableMasteries = [];
            var lockedMasteries = [];
 
            if (!masteryPanel) return;
 
            visibleMasteries = [];
 
            safeArray(data.masteries).forEach(function (cm) {
                var cmName = String(cm.name || '');
                var cmPath = normalizePath(cm.lycanthropy);
                var bitePath = getBitePath(cmName);
                var pathOk = false;
                var raceOk;
                var classOk;
                var tabOk;
                var searchOk;
                var lockReasons;
 
                raceOk = listAllowsValue(cm.races, selectedRaceCode);
                classOk = listAllowsValue(cm.classes, selectedClass);
                tabOk = String(getCmDisplayTab(cm)).toLowerCase() === String(activeCmTab).toLowerCase();
                searchOk = !searchQuery || getCmSearchText(cm).indexOf(searchQuery) !== -1;
 
                if (bitePath) {
                    pathOk = selectedPathNormalized === bitePath;
                } else if (!cmPath) {
                    pathOk = true;
                } else if (!selectedPathNormalized) {
                    pathOk = false;
                } else if (selectedIsWere && isSharedWereMastery(cmName)) {
                    pathOk = cmPath === 'werebeast' || isWerePath(cmPath);
                } else if (selectedIsWere && cmPath === 'werebeast') {
                    pathOk = true;
                } else {
                    pathOk = cmPath === selectedPathNormalized;
                }
 
                if (
                    !raceOk ||
                    !classOk ||
                    !tabOk ||
                    !pathOk ||
                    !searchOk ||
                    selectedMasteries[cmName] ||
                    seen[cmName]
                ) {
                    return;
                }
 
                seen[cmName] = true;
 
                lockReasons = getCmLockReasons(cm, false).concat(getClassPathLockReasons(cm));
 
                if (lockReasons.length) {
                    lockedMasteries.push({
                        cm: cm,
                        locked: true,
                        unlockHint: lockReasons.join(' | ')
                    });
                } else {
                    availableMasteries.push({
                        cm: cm,
                        locked: false,
                        unlockHint: ''
                    });
                }
            });
 
            visibleMasteries = showLockedCms ? availableMasteries.concat(lockedMasteries) : availableMasteries;
 
            if (!visibleMasteries.length) {
                masteryPanel.innerHTML =
                    'No Class Masteries found for this selection.<br>' +
                    '<small>Loaded masteries: ' + safeArray(data.masteries).length + '</small>';
                return;
            }
 
            masteryPanel.innerHTML = visibleMasteries.map(function (entry, index) {
                var cm = entry.cm;
                var lockedClass = entry.locked ? ' cm-card-locked' : '';
                var typeClass = getCmTypeClass(cm);
                var cmType = getCmType(cm);
var reqHtml = getRequirementsHtml(cm);
var statReqHtml = getStatReqHtml(cm);
var gainLossHtml = getCmGainLossHtml(cm);
var lockHtml = '';
var metaHtml;
 
                if (entry.locked) {
                    lockHtml =
                        '<div class="cm-unlock-hint">' +
                            '<strong>Locked:</strong><br>' +
                            escapeHtml(entry.unlockHint).replace(/\|/g, '<br>') +
                        '</div>';
                }
 
                metaHtml =
                    '<div class="cm-card-meta">' +
                        'Level: ' + escapeHtml(cm.level) +
                        ' | Cost: ' + escapeHtml(cm.points) +
                        (cm.cmSpentReq ? ' | CM Spent Req: ' + escapeHtml(cm.cmSpentReq) : '') +
                    '</div>' +
                    '<div class="cm-card-meta">' + escapeHtml(getCmPointStatusText()) + '</div>';
 
                if (activeCmView === 'compact') {
                    return '<div class="cm-card cm-purchasable-card cm-card-compact ' + typeClass + lockedClass + '" data-cm-index="' + index + '">' +
                        '<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
                        '<div class="cm-card-type-tag">' + escapeHtml(cmType) + '</div>' +
metaHtml +
gainLossHtml +
(statReqHtml ? '<div class="cm-card-req">Stat Req: ' + statReqHtml + '</div>' : '') +
lockHtml +
                    '</div>';
                }
 
                return '<div class="cm-card cm-purchasable-card ' + typeClass + lockedClass + '" data-cm-index="' + index + '">' +
                    '<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
                    '<div class="cm-card-type-tag">' + escapeHtml(cmType) + '</div>' +
'<div class="cm-card-desc">' + escapeHtml(cm.description) + '</div>' +
metaHtml +
gainLossHtml +
(reqHtml ? '<div class="cm-card-req">Requires: ' + reqHtml + '</div>' : '') +
                    (statReqHtml ? '<div class="cm-card-req">Stat Req: ' + statReqHtml + '</div>' : '') +
                    lockHtml +
                '</div>';
            }).join('');
 
            Array.prototype.forEach.call(masteryPanel.querySelectorAll('.cm-purchasable-card'), function (card) {
                var index = parseInt(card.getAttribute('data-cm-index'), 10);
                var entry = visibleMasteries[index];
                var cm = entry ? entry.cm : null;
 
          /*      card.addEventListener('mouseenter', function () {
                    showCmTooltip(cm, card);
                });
 
                card.addEventListener('mouseleave', hideCmTooltip); */
 
                card.addEventListener('click', function () {
                    var reasons;
                    var selectedTitle;
 
                    if (!cm || !entry) return;
 
                    if (entry.locked) {
                        alert(entry.unlockHint);
                        return;
                    }
 
                    reasons = getCmLockReasons(cm, true);
 
                    if (reasons.length) {
                        alert(reasons.join(' | '));
                        return;
                    }
 
                    if (isMasterClassTitle(cm)) {
    selectedTitle = getSelectedMasterClassTitle();
 
    if (selectedTitle && selectedTitle !== cm.name) {
        alert('You can only select one Master Class Title per character build.');
         return;
     }
}
 
 
var classPathReasons = getClassPathLockReasons(cm);
 
if (classPathReasons.length) {
     alert(classPathReasons.join(' | '));
    return;
}
 
 
selectedMasteries[cm.name] = cm;
                    hideCmTooltip();
                    updateSummary();
                });
            });
        }
 
 
 
 
          
 
         function setBaseStatInputs(baseStats) {
             baseStats = baseStats || {};
             document.getElementById('cm-base-str').value = numberOrZero(baseStats.STR);
             document.getElementById('cm-base-sta').value = numberOrZero(baseStats.STA);
             document.getElementById('cm-base-dex').value = numberOrZero(baseStats.DEX);
             document.getElementById('cm-base-agi').value = numberOrZero(baseStats.AGI);
             document.getElementById('cm-base-int').value = numberOrZero(baseStats.INT);
             document.getElementById('cm-base-wis').value = numberOrZero(baseStats.WIS);
             document.getElementById('cm-base-cha').value = numberOrZero(baseStats.CHA);
         }
 
         function loadBuildFromUrl() {
             var hash = window.location.hash || '';
             var build;
 
             if (hash.indexOf('#cm-build=') !== 0) return;


             try {
             try {
Line 1,519: Line 1,855:


             safeArray(build.cms).forEach(function (name) {
             safeArray(build.cms).forEach(function (name) {
                var cm = findCmByName(name);
var cm = findCmByNameOrId(name);
                 if (cm) selectedMasteries[cm.name] = cm;
                 if (cm) selectedMasteries[cm.name] = cm;
             });
             });
Line 1,561: Line 1,897:
                 renderMasteryList();
                 renderMasteryList();
             });
             });
        }
        if (shareUrlButton) {
            shareUrlButton.addEventListener('click', function () {
                copyTextToClipboard(buildShareUrl(), shareUrlButton);
            });
        }
        if (exportCardButton) {
            exportCardButton.addEventListener('click', renderBuildCard);
         }
         }


Line 1,607: Line 1,933:
         });
         });


         if (resetButton) {
         function resetClassMasterySelections() {
            resetButton.addEventListener('click', function () {
    selectedMasteries = {};
                selectedMasteries = {};
    sharedCmBuildApplied = false;
                activeCmTab = 'General';
    activeCmTab = 'General';
                activeCmView = 'detailed';
    activeCmView = 'detailed';
                showLockedCms = true;
    showLockedCms = true;
                levelInput.value = 60;
    levelInput.value = 60;
                setBaseStatInputs({});
    setBaseStatInputs({});
 
    if (pathSelect) pathSelect.value = '';
    if (searchInput) searchInput.value = '';
    if (showLockedInput) showLockedInput.checked = true;
 
    Array.prototype.forEach.call(document.querySelectorAll('.cm-tab-button'), function (btn) {
        btn.classList.remove('cm-tab-active');
    });


                if (pathSelect) pathSelect.value = '';
    Array.prototype.forEach.call(viewButtons, function (btn) {
                if (searchInput) searchInput.value = '';
        btn.classList.remove('cm-view-active');
                if (showLockedInput) showLockedInput.checked = true;
    });


                Array.prototype.forEach.call(document.querySelectorAll('.cm-tab-button'), function (btn) {
    var generalTab = document.querySelector('.cm-tab-button[data-tab="General"]');
                    btn.classList.remove('cm-tab-active');
    var detailedView = document.querySelector('.cm-view-button[data-view="detailed"]');
                });


                Array.prototype.forEach.call(viewButtons, function (btn) {
    if (generalTab) generalTab.classList.add('cm-tab-active');
                    btn.classList.remove('cm-view-active');
    if (detailedView) detailedView.classList.add('cm-view-active');
                });


                var generalTab = document.querySelector('.cm-tab-button[data-tab="General"]');
    hideCmTooltip();
                var detailedView = document.querySelector('.cm-view-button[data-view="detailed"]');
    updateSummary();
}


                if (generalTab) generalTab.classList.add('cm-tab-active');
window.EQOA_RESET_CLASS_MASTERY = resetClassMasterySelections;
                if (detailedView) detailedView.classList.add('cm-view-active');


                hideCmTooltip();
if (resetButton) {
                updateSummary();
    resetButton.addEventListener('click', resetClassMasterySelections);
            });
}
        }


         loadSavedCmTheme();
         loadSavedCmTheme();

Latest revision as of 22:28, 25 June 2026

mw.hook('wikipage.content').add(function () {
    var cmCalculatorStarted = false;

    function cmDataIsReady() {
        return !!window.EQOA_CM_DATA &&
            (
                (
                    window.EQOA_CM_DATA.masteries &&
                    window.EQOA_CM_DATA.masteries.length
                ) ||
                window.EQOA_CM_RAW
            );
    }

    function waitForCMReady(attemptsLeft) {
        var app = document.getElementById('cm-calculator-app');

        if (app && cmDataIsReady()) {
            if (cmCalculatorStarted) return;

            cmCalculatorStarted = true;
            initClassMasteryCalculator();
            return;
        }

        if (attemptsLeft <= 0) {
            if (app) {
                app.innerHTML =
                    '<div style="color:red;">EQOA Class Mastery failed to load.</div>';
            }

            return;
        }

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

    waitForCMReady(100);

    function initClassMasteryCalculator() {
        var app = document.getElementById('cm-calculator-app');
        if (!app) return;

        var data = window.EQOA_CM_DATA || {};
var selectedMasteries = {};
var sharedCmBuildApplied = false;
var activeCmTab = 'General';
var activeCmView = 'detailed';
var showLockedCms = true;
var visibleMasteries = [];

        function safeArray(value) {
            return Object.prototype.toString.call(value) === '[object Array]' ? value : [];
        }

        function numberOrZero(value) {
            var n = Number(String(value || '').trim());
            return isFinite(n) ? n : 0;
        }

        function escapeHtml(value) {
            return String(value == null ? '' : value)
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;')
                .replace(/'/g, '&#039;');
        }

        function splitList(value) {
            value = String(value || '').trim();
            if (!value || value.toLowerCase() === 'all') return ['All'];
            return value.split(/\s+/);
        }

        function buildMasteriesFromRaw(rawText) {
            var lines = String(rawText || '').split(/\r?\n/);
            var headerLineIndex = -1;
            var headers = [];
            var headerMap = {};
            var rows = [];

            function get(cols, name) {
                var indexes = headerMap[name] || [];
                if (!indexes.length) return '';
                return String(cols[indexes[0]] || '').trim();
            }

            function getAll(cols, name) {
                var indexes = headerMap[name] || [];
                var values = [];

                indexes.forEach(function (index) {
                    var value = String(cols[index] || '').trim();

                    if (!value) return;
                    if (/^[0-9A-F]{4}$/i.test(value)) return;

                    if (values.indexOf(value) === -1) {
                        values.push(value);
                    }
                });

                return values;
            }


function getAllHeadersContaining(cols, text) {
    var values = [];
    text = String(text || '').toLowerCase();

    headers.forEach(function (header, index) {
        var value;

        if (String(header || '').toLowerCase().indexOf(text) === -1) return;

        value = String(cols[index] || '').trim();

        if (!value) return;
        if (/^[0-9A-F]{4}$/i.test(value)) return;

        if (values.indexOf(value) === -1) {
            values.push(value);
        }
    });

    return values;
}



            function addStat(stats, cols, header, key) {
                var value = parseInt(get(cols, header), 10);
                if (!isNaN(value) && value !== 0) {
                    stats[key] = value;
                }
            }

            function addStatAny(stats, cols, headers, key) {
                var found = false;

                headers.forEach(function (header) {
                    var value;

                    if (found) return;

                    value = parseInt(get(cols, header), 10);

                    if (!isNaN(value) && value !== 0) {
                        stats[key] = value;
                        found = true;
                    }
                });
            }

            lines.forEach(function (line, index) {
if (
    headerLineIndex === -1 &&
    line.indexOf('Mastery\tDescription') !== -1
) {
    headerLineIndex = index;
}
            });

            if (headerLineIndex === -1) return [];

            headers = lines[headerLineIndex].split('\t').map(function (header) {
                return String(header || '').trim();
            });

            headers.forEach(function (header, index) {
                if (!headerMap[header]) headerMap[header] = [];
                headerMap[header].push(index);
            });

            lines.slice(headerLineIndex + 1).forEach(function (line) {
                var cols;
                var name;
var stats = {};
var baseStatReqs = {};
var cmId = '';

                if (!line.trim()) return;

cols = line.split('\t');
name = get(cols, 'Mastery');

cmId =
    get(cols, 'CmID') ||
    get(cols, 'CMID') ||
    get(cols, 'CM ID') ||
    get(cols, 'Cm ID');

if (!cmId && /^[0-9A-F]{4}$/i.test(String(cols[0] || '').trim())) {
    cmId = String(cols[0] || '').trim();
}

if (!name) return;

addStat(stats, cols, 'AC', 'ac');
addStat(stats, cols, 'STR', 'str');
addStat(stats, cols, 'STA', 'sta');
addStat(stats, cols, 'AGI', 'agi');
addStat(stats, cols, 'DEX', 'dex');
addStat(stats, cols, 'WIS', 'wis');
addStat(stats, cols, 'INT', 'int');
addStat(stats, cols, 'CHA', 'cha');

addStat(stats, cols, 'HP Max', 'hp');
addStat(stats, cols, 'PWR Max', 'pow');
addStat(stats, cols, 'POW Max', 'pow');
addStat(stats, cols, 'Power Max', 'pow');

/* Resists */
addStat(stats, cols, 'PR', 'pr');
addStat(stats, cols, 'DR', 'dr');
addStat(stats, cols, 'FR', 'fr');
addStat(stats, cols, 'CR', 'cr');
addStat(stats, cols, 'LR', 'lr');
addStat(stats, cols, 'AR', 'ar');

/* Regen / resource ticks */
addStat(stats, cols, 'HoT', 'hot');
addStat(stats, cols, 'HOT', 'hot');
addStat(stats, cols, 'PoT', 'pot');
addStat(stats, cols, 'POT', 'pot');

/* Modifiers */
addStat(stats, cols, 'HP Factor', 'hpFactor');
addStat(stats, cols, 'OFF Mod', 'offMod');
addStat(stats, cols, 'DEF Mod', 'defMod');
addStat(stats, cols, 'Speed', 'speed');

/* Stat cap increases */
addStatAny(stats, cols, ['STR Max', 'Max STR', 'STR MAX'], 'strMax');
addStatAny(stats, cols, ['STA Max', 'Max STA', 'STA MAX'], 'staMax');
addStatAny(stats, cols, ['AGI Max', 'Max AGI', 'AGI MAX'], 'agiMax');
addStatAny(stats, cols, ['DEX Max', 'Max DEX', 'DEX MAX'], 'dexMax');
addStatAny(stats, cols, ['WIS Max', 'Max WIS', 'WIS MAX'], 'wisMax');
addStatAny(stats, cols, ['INT Max', 'Max INT', 'INT MAX'], 'intMax');
addStatAny(stats, cols, ['CHA Max', 'Max CHA', 'CHA MAX'], 'chaMax');
                ['STR', 'STA', 'DEX', 'AGI', 'INT', 'WIS', 'CHA'].forEach(function (stat) {
                    var value = parseInt(get(cols, 'Base ' + stat + ' Req'), 10);
                    if (!isNaN(value) && value > 0) {
                        baseStatReqs[stat] = value;
                    }
                });

rows.push({
    id: cmId,
    name: name,
                    description: get(cols, 'Description'),
                    classification: get(cols, 'Classification'),
                    subCategory: get(cols, 'Sub-Category'),
playerFlags: []
    .concat(getAll(cols, 'Playerflags'))
    .concat(getAll(cols, 'Player Flags'))
    .concat(getAll(cols, 'Playerflag'))
    .concat(getAll(cols, 'Player Flag'))
    .concat(getAllHeadersContaining(cols, 'playerflag'))
    .concat(getAllHeadersContaining(cols, 'player flag')),
                    races: splitList(get(cols, 'Race')),
                    classes: splitList(get(cols, 'Class')),
                    lycanthropy: get(cols, 'Lycanthropy'),

                    quest: get(cols, 'Quest'),
                    level: parseInt(get(cols, 'Level'), 10) || 0,
                    points: parseInt(get(cols, 'Points'), 10) || 0,

                    cmSpentReq: parseInt(get(cols, 'CM Spent Req'), 10) || 0,
                    requirements: []
                        .concat(getAll(cols, 'CM Req 1'))
                        .concat(getAll(cols, 'CM Req 2'))
                        .concat(getAll(cols, 'CM Req 3'))
                        .concat(getAll(cols, 'CM Req 4')),
                    spellsAdded: []
                        .concat(getAll(cols, '=+Spell 1'))
                        .concat(getAll(cols, '=+Spell 2'))
                        .concat(getAll(cols, '=+Spell 3'))
                        .concat(getAll(cols, '=+Spell 4'))
                        .concat(getAll(cols, '=+Spell 5'))
                        .concat(getAll(cols, '=+Spell 6')),
                    spellsRemoved: []
                        .concat(getAll(cols, '-Spell 1'))
                        .concat(getAll(cols, '-Spell 2'))
                        .concat(getAll(cols, '-Spell 3'))
                        .concat(getAll(cols, '-Spell 4'))
                        .concat(getAll(cols, '-Spell 5'))
                        .concat(getAll(cols, '-Spell 6')),
                    stats: stats,
                    baseStatReqs: baseStatReqs
                });
            });

            return rows;
        }


function buildCmIdsByNameFromRaw(rawText) {
    var idsByName = {};
    var lines = String(rawText || '').split(/\r?\n/);

    lines.forEach(function (line) {
        var cols;
        var id;
        var name;

        line = String(line || '').trim();

        if (!line) return;

        if (line.indexOf('\t') !== -1) {
            cols = line.split('\t');
        } else if (line.indexOf('\\t') !== -1) {
            cols = line.split('\\t');
        } else {
            cols = line.split(/\s{2,}/);
        }

        id = String(cols[0] || '').trim().replace(/[^0-9A-F]/gi, '');
        name = String(cols[1] || '').trim();

        if (!/^[0-9A-F]{4}$/i.test(id)) return;
        if (!name || name.toLowerCase() === 'mastery') return;

        idsByName[name] = id;
    });

    return idsByName;
}

if (window.EQOA_CM_RAW) {
    var rawMasteriesWithIds = buildMasteriesFromRaw(window.EQOA_CM_RAW);
    var cmIdsByName = buildCmIdsByNameFromRaw(window.EQOA_CM_IDS_RAW || window.EQOA_CM_RAW);
    var rawMasteriesByName = {};

    rawMasteriesWithIds.forEach(function (cm) {
        if (cm.name && cmIdsByName[cm.name]) {
            cm.id = cmIdsByName[cm.name];
        }

        if (cm.name) {
            rawMasteriesByName[cm.name] = cm;
        }
    });

    if (!data.masteries || !data.masteries.length) {
        data.masteries = rawMasteriesWithIds;
    } else {
        data.masteries.forEach(function (cm) {
            var rawCm;

            if (cm.name && cmIdsByName[cm.name]) {
                cm.id = cmIdsByName[cm.name];
            }

            rawCm = cm.name ? rawMasteriesByName[cm.name] : null;

            if (rawCm && rawCm.stats) {
                cm.stats = cm.stats || {};

                Object.keys(rawCm.stats).forEach(function (statKey) {
                    cm.stats[statKey] = rawCm.stats[statKey];
                });
            }
        });
    }

    if (window.EQOA_CM_DATA) {
        window.EQOA_CM_DATA.masteries = data.masteries;
    }
}


        function getBuilderState() {
            if (
                window.EQOA_CHARACTER_BUILDER_STATE &&
                typeof window.EQOA_CHARACTER_BUILDER_STATE.getState === 'function'
            ) {
                return window.EQOA_CHARACTER_BUILDER_STATE.getState();
            }

            return null;
        }

function getSelectedTransformationPath() {
    var builderState = getBuilderState();

    if (builderState && builderState.transformationPath !== undefined) {
        return builderState.transformationPath;
    }

    return pathSelect ? pathSelect.value : '';
}

        function isEmbedded() {
            return !!getBuilderState();
        }

        function getRaceCode(raceName) {
            var raceCodes = data.raceCodes || {};
            return raceCodes[raceName] || raceName;
        }

        function listAllowsValue(list, value) {
            var wanted = String(value || '').toUpperCase();

            if (!wanted) return true;

            return safeArray(list).some(function (entry) {
                var text = String(entry || '').toUpperCase().trim();
                var tokens;

                if (!text || text === 'ALL') return true;

                tokens = text.split(/\s+/);
                return tokens.indexOf(wanted) !== -1;
            });
        }

        function normalizePath(value) {
            value = String(value || '').toLowerCase().replace(/[\s-]/g, '');

            if (!value || value === 'all' || value === 'none') return '';

            var map = {
                lionwere: 'werelion',
                werelion: 'werelion',
                bearwere: 'werebear',
                werebear: 'werebear',
                wolfwere: 'werewolf',
                werewolf: 'werewolf',
                ratwere: 'wererat',
                wererat: 'wererat',
                gatorwere: 'weregator',
                weregator: 'weregator',
                vampire: 'vampire',
                werehunter: 'werehunter',
                hunterwere: 'werehunter',
                werebeast: 'werebeast'
            };

            return map[value] || value;
        }

        function isWerePath(path) {
            return ['werelion', 'werebear', 'werewolf', 'wererat', 'weregator'].indexOf(path) !== -1;
        }

        function getBitePath(name) {
            name = String(name || '').toLowerCase();

            if (name.indexOf('lion bite') !== -1) return 'werelion';
            if (name.indexOf('bear bite') !== -1) return 'werebear';
            if (name.indexOf('wolf bite') !== -1) return 'werewolf';
            if (name.indexOf('rat bite') !== -1) return 'wererat';
            if (name.indexOf('gator bite') !== -1) return 'weregator';

            return '';
        }

        function isSharedWereMastery(name) {
            return name === 'Lycanthropic Resistances' ||
                name === 'Lycanthropic Regeneration' ||
                name === 'Lycanthropic Rejuvenation' ||
                name === 'Combat Senses' ||
                name === 'Defensive Instincts';
        }

        function formatStatName(statName) {
            var map = {
                ac: 'AC',
                hp: 'HP',
                pow: 'Power',
                str: 'STR',
                sta: 'STA',
                agi: 'AGI',
                dex: 'DEX',
                wis: 'WIS',
                int: 'INT',
                cha: 'CHA',

pr: 'PR',
dr: 'DR',
fr: 'FR',
cr: 'CR',
lr: 'LR',
ar: 'AR',
hot: 'HoT',
pot: 'PoT',
hpFactor: 'HP Factor',
offMod: 'OFF Mod',
defMod: 'DEF Mod',
speed: 'Speed',

                strMax: 'STR Max',
                staMax: 'STA Max',
                agiMax: 'AGI Max',
                dexMax: 'DEX Max',
                wisMax: 'WIS Max',
                intMax: 'INT Max',
                chaMax: 'CHA Max'
            };

            return map[statName] || String(statName || '').toUpperCase();
        }

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

        function getCmDisplayTab(cm) {
            var classification = String((cm && cm.classification) || '');

            if (classification === 'Master Class') {
                if (safeArray(cm.races).length && safeArray(cm.races)[0] !== 'All') return 'Racial';
                if (safeArray(cm.classes).length && safeArray(cm.classes)[0] !== 'All') return 'Class';
            }

            return classification || 'General';
        }

        function getCmType(cm) {
            return getCmDisplayTab(cm) || 'Other';
        }

        function getCmTypeClass(cm) {
            var type = getCmType(cm);

            if (type === 'General') return 'cm-type-general';
            if (type === 'Racial') return 'cm-type-racial';
            if (type === 'Archetype') return 'cm-type-archetype';
            if (type === 'Class') return 'cm-type-class';
            if (type === 'Master Class') return 'cm-type-master';

            return 'cm-type-other';
        }

        function getBaseStatsFromInputs() {
            var builderState = getBuilderState();
            var baseStats;
            var result;

            if (builderState && builderState.baseStats) {
                baseStats = builderState.baseStats;
            } else {
                baseStats = {
                    STR: numberOrZero(document.getElementById('cm-base-str') && document.getElementById('cm-base-str').value),
                    STA: numberOrZero(document.getElementById('cm-base-sta') && document.getElementById('cm-base-sta').value),
                    DEX: numberOrZero(document.getElementById('cm-base-dex') && document.getElementById('cm-base-dex').value),
                    AGI: numberOrZero(document.getElementById('cm-base-agi') && document.getElementById('cm-base-agi').value),
                    INT: numberOrZero(document.getElementById('cm-base-int') && document.getElementById('cm-base-int').value),
                    WIS: numberOrZero(document.getElementById('cm-base-wis') && document.getElementById('cm-base-wis').value),
                    CHA: numberOrZero(document.getElementById('cm-base-cha') && document.getElementById('cm-base-cha').value)
                };
            }

            result = {
                STR: numberOrZero(baseStats.STR),
                STA: numberOrZero(baseStats.STA),
                DEX: numberOrZero(baseStats.DEX),
                AGI: numberOrZero(baseStats.AGI),
                INT: numberOrZero(baseStats.INT),
                WIS: numberOrZero(baseStats.WIS),
                CHA: numberOrZero(baseStats.CHA)
            };

            Object.keys(selectedMasteries).forEach(function (name) {
                var stats = selectedMasteries[name].stats || {};
                result.STR += numberOrZero(stats.str);
                result.STA += numberOrZero(stats.sta);
                result.DEX += numberOrZero(stats.dex);
                result.AGI += numberOrZero(stats.agi);
                result.INT += numberOrZero(stats.int);
                result.WIS += numberOrZero(stats.wis);
                result.CHA += numberOrZero(stats.cha);
            });

            return result;
        }

        function updateBaseStatInputsDisplay(baseStats) {
            var map = {
                STR: 'cm-base-str',
                STA: 'cm-base-sta',
                DEX: 'cm-base-dex',
                AGI: 'cm-base-agi',
                INT: 'cm-base-int',
                WIS: 'cm-base-wis',
                CHA: 'cm-base-cha'
            };

            Object.keys(map).forEach(function (stat) {
                var input = document.getElementById(map[stat]);
                if (input) input.value = numberOrZero(baseStats[stat]);
            });
        }

        function getAvailablePoints() {
            var builderState = getBuilderState();
            var level = parseInt(levelInput.value, 10);
            var max;
            var available;

            if (isNaN(level) || level < 1) level = 1;
            if (level > 60) level = 60;

            levelInput.value = level;
            max = Math.min(level * 25, 1500);

            if (builderState && builderState.cmAvailable !== undefined) {
                available = numberOrZero(builderState.cmAvailable);
                if (available < 0) available = 0;
                if (available > max) available = max;
                return available;
            }

            return max;
        }

        function getSpentPoints() {
            var total = 0;

            Object.keys(selectedMasteries).forEach(function (name) {
                total += numberOrZero(selectedMasteries[name].points);
            });

            return total;
        }

        function getCmPointStatusText() {
            var total = getAvailablePoints();
            var spent = getSpentPoints();
            var left = total - spent;

            if (left < 0) left = 0;

            return 'Total CMs: ' + total + ' | Spent: ' + spent + ' | Left: ' + left;
        }

        function getCmStatMinRequirements(cm) {
            var reqs = [];

            if (!cm || !cm.baseStatReqs) return reqs;

            Object.keys(cm.baseStatReqs).forEach(function (statName) {
                var value = numberOrZero(cm.baseStatReqs[statName]);
                if (value > 0) {
                    reqs.push({ stat: statName, value: value });
                }
            });

            return reqs;
        }

        function getCmLockReasons(cm, includePoints) {
            var spent = getSpentPoints();
            var available = getAvailablePoints();
            var baseStats = getBaseStatsFromInputs();
            var level = parseInt(levelInput.value, 10) || 1;
            var reasons = [];

            if (cm.level && cm.level > level) {
                reasons.push('Reach level ' + cm.level);
            }

            if (cm.cmSpentReq && spent < cm.cmSpentReq) {
                reasons.push('Spend ' + (cm.cmSpentReq - spent) + ' more CM');
            }

            getCmStatMinRequirements(cm).forEach(function (req) {
                if (numberOrZero(baseStats[req.stat]) < req.value) {
                    reasons.push('Requires base ' + req.stat + ' ' + req.value);
                }
            });

            safeArray(cm.requirements).forEach(function (reqName) {
                if (!selectedMasteries[reqName]) {
                    reasons.push('Requires ' + reqName);
                }
            });

            if (includePoints && spent + numberOrZero(cm.points) > available) {
                reasons.push('Not enough CM points');
            }

            return reasons;
        }

        function getRequirementsHtml(cm) {
            return safeArray(cm.requirements).map(function (reqName) {
                return '<span class="' + (selectedMasteries[reqName] ? 'cm-req-owned' : 'cm-req-missing') + '">' +
                    escapeHtml(reqName) +
                '</span>';
            }).join(', ');
        }

        function getStatReqHtml(cm) {
            return getCmStatMinRequirements(cm).map(function (req) {
                return 'Base ' + escapeHtml(req.stat) + ' ' + escapeHtml(req.value);
            }).join(', ');
        }

function getCmPassiveEffectText(cm) {
    var fields = [
        cm.effect,
        cm.effects,
        cm.effectText,
        cm.bonus,
        cm.passive,
        cm.description
    ];

    var found = '';

    fields.forEach(function (value) {
        value = String(value || '').trim();

        if (!found && value && value.toLowerCase() !== 'none') {
            found = value;
        }
    });

    return found;
}

function getCmGainLossHtml(cm) {
    var parts = [];

    Object.keys(cm.stats || {}).sort().forEach(function (key) {
        var value = numberOrZero(cm.stats[key]);
        var className;

        if (!value) return;

        className = value > 0 ? 'cm-card-gain' : 'cm-card-loss';

        parts.push(
            '<span class="' + className + '">' +
                escapeHtml(formatStatName(key)) + ' ' +
                (value > 0 ? '+' : '') +
                escapeHtml(value) +
            '</span>'
        );
    });

    safeArray(cm.spellsAdded).forEach(function (spell) {
        if (!spell) return;

        parts.push(
            '<span class="cm-card-gain">+ ' +
                escapeHtml(spell) +
            '</span>'
        );
    });

    safeArray(cm.spellsRemoved).forEach(function (spell) {
        if (!spell) return;

        parts.push(
            '<span class="cm-card-loss">- ' +
                escapeHtml(spell) +
            '</span>'
        );
    });

    if (!parts.length) {
        var passiveText = getCmPassiveEffectText(cm);

        if (passiveText) {
            parts.push(
                '<span class="cm-card-effect-note">' +
                    escapeHtml(passiveText) +
                '</span>'
            );
        }
    }

if (cm.description) {
    parts.unshift(
        '<span class="cm-card-description-chip">' +
            escapeHtml(cm.description) +
        '</span>'
    );
}

    if (!parts.length) return '';

    return '<div class="cm-card-effects">' + parts.join('') + '</div>';
}

        function getCmSearchText(cm) {
            var parts = [
                cm.name,
                cm.description,
                cm.classification,
                cm.lycanthropy
            ];

            Object.keys(cm.stats || {}).forEach(function (key) {
                parts.push(key);
                parts.push(formatStatName(key));
                parts.push(cm.stats[key]);
            });

            safeArray(cm.requirements).forEach(function (value) { parts.push(value); });
            safeArray(cm.spellsAdded).forEach(function (value) { parts.push(value); });
            safeArray(cm.spellsRemoved).forEach(function (value) { parts.push(value); });

            return parts.join(' ').toLowerCase();
        }









function findCmByNameOrId(value) {
    var found = null;
    var wanted = String(value || '').trim().toLowerCase();

    if (!wanted) return null;

    safeArray(data.masteries).some(function (cm) {
        var cmName = String(cm.name || '').trim().toLowerCase();
        var cmId = String(cm.id || cm.cmId || cm.CmID || '').trim().toLowerCase();

        if (cmName === wanted || cmId === wanted) {
            found = cm;
            return true;
        }

        return false;
    });

    return found;
}

var masterClassTitles = [
    'Berserker',
    'Chosen',
    'Deputy',
    'Doomseeker',
    'Immortal',
    'Juggernaut',
    'Mercenary',
    'Savage',
    'Scholar',
    'Tinker',

    'Arcane Were-Hunter',
    'Divine Were-Hunter',
    'Gypsy Were-Hunter',
    'Heroic Were-Hunter',
    'Metabolist',
    'Transmuter',
    'Gypsy',
    'Minstrel',
    'Exorcist',
    'Faithful',
    'Storm Caller',
    'Wilding',
    'Animator',

    'Spellbinder',
    'Aeromancer',
    'Geomancer',
    'Hydromancer',
    'Pyromancer',
    'Ninja',
    'Sensei',

    'Defiler',
    'Lich',
    'Crusader',
    'Guardian',
    'Forester',
    'Hunter',
    'Assassin',
    'Burglar',
    'Death Knight',
    'Slayer',

    'Elder',
    'Mystic',
    'Defender',

    'Hero',
    'Channeler',
    'Sorcerer',

    'Bravo',
    'Diviner',
    'Sage',
    'Soldier'
];

function normalizeMasterClassName(value) {
    return String(value || '')
        .toLowerCase()
        .replace(/[\s\u00a0]+/g, ' ')
        .trim();
}

function isMasterClassTitle(cm) {
    var cmName;

    if (!cm || !cm.name) return false;

    cmName = normalizeMasterClassName(cm.name);

    return masterClassTitles.some(function (title) {
        return normalizeMasterClassName(title) === cmName;
    });
}











function applySharedClassMasteriesFromBuilder(builderState) {
    var names;

    if (!builderState || sharedCmBuildApplied) return;

    names = safeArray(builderState.selectedClassMasteries);

    if (!names.length) return;

    selectedMasteries = {};

    names.forEach(function (name) {
var cm = findCmByNameOrId(name);

        if (cm) {
            selectedMasteries[cm.name] = cm;
        }
    });

    sharedCmBuildApplied = true;
}



        function getSelectedMasterClassTitle() {
    var selectedTitle = '';

    Object.keys(selectedMasteries).forEach(function (name) {
        if (isMasterClassTitle(selectedMasteries[name])) {
            selectedTitle = name;
        }
    });

    return selectedTitle;
}







function getCmPlayerFlags(cm) {
    var flags = [];

    if (!cm) return flags;

    safeArray(cm.playerFlags).forEach(function (flag) {
        flags.push(flag);
    });

    Object.keys(cm).forEach(function (key) {
        if (String(key).toLowerCase().indexOf('playerflag') !== -1) {
            if (typeof cm[key] === 'string') {
                flags.push(cm[key]);
            } else {
                safeArray(cm[key]).forEach(function (flag) {
                    flags.push(flag);
                });
            }
        }
    });

    return flags;
}

function getRomanRank(value) {
    var match = String(value || '').trim().match(/\s+(I|II|III|IV|V)$/i);

    if (!match) return 0;

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

function stripRankSuffix(value) {
    return String(value || '')
        .trim()
        .replace(/\s+(I|II|III|IV|V)$/i, '');
}

function normalizePathKey(value) {
    return stripRankSuffix(value)
        .toLowerCase()
        .replace(/[^a-z0-9]/g, '');
}







function getExemplarFlag(cm) {
    var found = '';
    var questText = String((cm && cm.quest) || '').trim();

    if (/^exemplar-/i.test(questText)) {
        return questText;
    }

    getCmPlayerFlags(cm).forEach(function (flag) {
        var text = String(flag || '').trim();

        if (!found && /^exemplar-/i.test(text)) {
            found = text;
        }
    });

    return found;
}

function getPathFlag(cm) {
    return getExemplarFlag(cm);
}

function isClassPathCm(cm) {
    return !!getExemplarFlag(cm);
}

function getClassPathKey(cm) {
    var flag = getExemplarFlag(cm);

    if (flag) {
        return normalizePathKey(flag);
    }

    return normalizePathKey(cm && cm.name);
}








function getSelectedClassPathKey() {
    var selectedKey = '';

    Object.keys(selectedMasteries).forEach(function (name) {
        var cm = selectedMasteries[name];

        if (!selectedKey && isClassPathCm(cm)) {
            selectedKey = getClassPathKey(cm);
        }
    });

    return selectedKey;
}

function getSelectedExemplarPathChoice() {
    var selectedChoice = '';

    Object.keys(selectedMasteries).forEach(function (name) {
        var cm = selectedMasteries[name];

        if (!selectedChoice && isClassPathCm(cm)) {
            selectedChoice = stripRankSuffix(cm.name);
        }
    });

    return selectedChoice;
}

function getPreviousRankName(cm) {
    var rank = getRomanRank(cm && cm.name);
    var baseName = stripRankSuffix(cm && cm.name);
    var previousRank;

    if (rank <= 1) return '';

    previousRank = {
        2: 'I',
        3: 'II',
        4: 'III',
        5: 'IV'
    }[rank];

    return previousRank ? baseName + ' ' + previousRank : '';
}

function getClassPathLockReasons(cm) {
    var reasons = [];
    var selectedPathKey;
    var thisPathKey;
    var previousRankName;

    if (!isClassPathCm(cm)) return reasons;

    selectedPathKey = getSelectedClassPathKey();
    thisPathKey = getClassPathKey(cm);
    previousRankName = getPreviousRankName(cm);

    if (selectedPathKey && thisPathKey && selectedPathKey !== thisPathKey) {
        reasons.push('You already selected a different Class / Exemplar Path');
    }

    if (previousRankName && !selectedMasteries[previousRankName]) {
        reasons.push('Requires ' + previousRankName + ' first');
    }

    return reasons;
}








app.innerHTML =

            '<div class="cm-calculator-box">' +
                '<div class="character-builder-title" style="display:none;">EQOA: Sandstorm Class Mastery Builder</div>' +

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

                    '<label>Class</label>' +
                    '<select id="cm-class"></select>' +

                    '<label>Character Level</label>' +
                    '<input type="number" id="cm-level" min="1" max="60" value="60" style="width:70px;">' +

                    '<label>Base Stats</label>' +
                    '<span>' +
                        'STR <input type="number" id="cm-base-str" min="0" value="0" style="width:55px;"> ' +
                        'STA <input type="number" id="cm-base-sta" min="0" value="0" style="width:55px;"> ' +
                        'DEX <input type="number" id="cm-base-dex" min="0" value="0" style="width:55px;"> ' +
                        'AGI <input type="number" id="cm-base-agi" min="0" value="0" style="width:55px;"> ' +
                        'INT <input type="number" id="cm-base-int" min="0" value="0" style="width:55px;"> ' +
                        'WIS <input type="number" id="cm-base-wis" min="0" value="0" style="width:55px;"> ' +
                        'CHA <input type="number" id="cm-base-cha" min="0" value="0" style="width:55px;">' +
                    '</span>' +
         

                    '<button type="button" id="cm-reset" class="cm-reset-button">Reset</button>' +
                '</div>' +

               '<div class="character-builder-section-title" style="margin-top:15px;">Point Summary</div>' +
'<table class="character-builder-stat-table">' +
    '<tr><th>Available</th><th>Spent</th><th>Remaining</th></tr>' +
    '<tr><td id="cm-available">1500</td><td id="cm-spent">0</td><td id="cm-remaining">1500</td></tr>' +
'</table>' +

'<div class="character-builder-section-title cm-master-class-heading" style="margin-top:18px;">Master Class Title</div>' +
'<div id="cm-master-class-panel" class="cm-master-class-panel"><span class="cm-master-class-text">-</span></div>' +

'<div class="character-builder-section-title" style="margin-top:18px;">Stats / Spells Gained from CM</div>' +
                '<div id="cm-unlocked-summary">No stats or spells gained yet.</div>' +

                '<div class="character-builder-section-title" style="margin-top:18px;">Class Masteries</div>' +
                '<div class="cm-tabs">' +
                    '<button type="button" class="cm-tab-button cm-tab-active" data-tab="General">General</button>' +
                    '<button type="button" class="cm-tab-button" data-tab="Racial">Racial</button>' +
                    '<button type="button" class="cm-tab-button" data-tab="Archetype">Archetype</button>' +
                    '<button type="button" class="cm-tab-button" data-tab="Class">Class</button>' +
                '</div>' +

                '<div class="cm-search-tools">' +
                    '<div class="cm-search-row">' +
                        '<input type="search" id="cm-search" class="cm-search-input" placeholder="Search mastery, stat, spell, requirement...">' +
                        '<button type="button" id="cm-clear-search" class="cm-clear-search-button">Clear</button>' +
                    '</div>' +
                    '<label class="cm-lock-toggle"><input type="checkbox" id="cm-show-locked" checked> <span>Show locked</span></label>' +
                '</div>' +

                '<div class="cm-view-toggle">' +
                    '<button type="button" class="cm-view-button cm-view-active" data-view="detailed">Detailed</button>' +
                    '<button type="button" class="cm-view-button" data-view="compact">Compact</button>' +
                '</div>' +

               '<div class="cm-main-layout">' +
    '<div class="cm-purchased-panel">' +
        '<div class="cm-panel-title">Master Class</div>' +
        '<div id="cm-master-class-purchased-list">None selected.</div>' +
        '<div class="cm-panel-title" style="margin-top:12px;">CM Purchased</div>' +
        '<div id="cm-purchased-list">None purchased.</div>' +
    '</div>' +
    '<div id="cm-mastery-panel" class="character-builder-warning-box">Select a race and class to begin.</div>' +
'</div>' +

                '<div id="cm-tooltip" class="cm-tooltip" style="display:none;"></div>' +
            '</div>';

        var raceSelect = document.getElementById('cm-race');
        var classSelect = document.getElementById('cm-class');
        var levelInput = document.getElementById('cm-level');
        var pathSelect = document.getElementById('cm-path');
        var resetButton = document.getElementById('cm-reset');
        var searchInput = document.getElementById('cm-search');
        var clearSearchButton = document.getElementById('cm-clear-search');
        var showLockedInput = document.getElementById('cm-show-locked');
        var viewButtons = document.querySelectorAll('.cm-view-button');
        var cmTooltip = document.getElementById('cm-tooltip');


        function applyCmTheme(theme) {
            app.classList.remove('cm-theme-dark');
            app.classList.remove('cm-theme-light');
            app.classList.add(theme === 'light' ? 'cm-theme-light' : 'cm-theme-dark');
        }

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

            try {
                var saved = localStorage.getItem('eqoa-wiki-theme');
                if (saved === 'light') isLight = true;
                if (saved === 'dark') isLight = false;
            } catch (e) {}

            applyCmTheme(isLight ? 'light' : 'dark');
        }

        function loadRaces() {
            raceSelect.innerHTML = '';

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

            loadClasses();
        }

        function loadClasses() {
            var race = raceSelect.value;
            var classList = safeArray((data.raceClasses || {})[race]);

            classSelect.innerHTML = '';

            classList.forEach(function (classCode) {
                addOption(
                    classSelect,
                    classCode,
                    classCode + ' - ' + ((data.classes || {})[classCode] || classCode)
                );
            });

            updateSummary();
        }

        function syncFromCharacterBuilder() {
            var builderState = getBuilderState();

            if (!builderState) return;

            if (builderState.race && data.raceClasses[builderState.race]) {
                raceSelect.value = builderState.race;
                loadClasses();
            }

            if (builderState.classCode) {
                classSelect.value = builderState.classCode;
            }

            if (builderState.level !== undefined) {
                levelInput.value = builderState.level;
            }

            if (builderState.baseStats) {
    updateBaseStatInputsDisplay(builderState.baseStats);
}

applySharedClassMasteriesFromBuilder(builderState);

updateSummary();
        }

        window.EQOA_SYNC_CLASS_MASTERY_FROM_BUILDER = syncFromCharacterBuilder;

        if (isEmbedded()) {
            raceSelect.disabled = true;
            classSelect.disabled = true;
            levelInput.disabled = true;
        }

        function getBuildTotals() {
            var statTotals = {};
            var spellsAdded = {};
            var spellsRemoved = {};

            Object.keys(selectedMasteries).forEach(function (name) {
                var cm = selectedMasteries[name];

                Object.keys(cm.stats || {}).forEach(function (statName) {
                    var value = parseInt(cm.stats[statName], 10);

                    if (!isNaN(value) && value !== 0) {
                        statTotals[statName] = (statTotals[statName] || 0) + value;
                    }
                });

                safeArray(cm.spellsAdded).forEach(function (spell) {
                    if (spell) spellsAdded[spell] = true;
                });

                safeArray(cm.spellsRemoved).forEach(function (spell) {
                    if (spell) spellsRemoved[spell] = true;
                });
            });

            return {
                stats: statTotals,
                spellsAdded: spellsAdded,
                spellsRemoved: spellsRemoved
            };
        }

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

    Object.keys(selectedMasteries).forEach(function (name) {
        var stats = selectedMasteries[name].stats || {};

        caps.str += numberOrZero(stats.strMax);
        caps.sta += numberOrZero(stats.staMax);
        caps.agi += numberOrZero(stats.agiMax);
        caps.dex += numberOrZero(stats.dexMax);
        caps.wis += numberOrZero(stats.wisMax);
        caps.int += numberOrZero(stats.intMax);
        caps.cha += numberOrZero(stats.chaMax);
    });

    return caps;
}

function getSelectedMasteryByType(typeName) {
    var found = '';

    Object.keys(selectedMasteries).sort().forEach(function (name) {
        var cm = selectedMasteries[name];
        var type = getCmType(cm);

        if (!found && type === typeName) {
            found = name;
        }
    });

    return found;
}

function getClassMasteryExportData() {
    var totals = getBuildTotals();

    return {
        race: raceSelect ? raceSelect.value : '',
        classCode: classSelect ? classSelect.value : '',
        level: levelInput ? levelInput.value : '',
        path: getSelectedTransformationPath(),
        classPathChoice: getSelectedMasteryByType('Class'),
        exemplarPathChoice: getSelectedExemplarPathChoice(),
        available: getAvailablePoints(),
        spent: getSpentPoints(),
        remaining: Math.max(0, getAvailablePoints() - getSpentPoints()),
        masterClassTitle: getSelectedMasterClassTitle(),
selectedMasteries: Object.keys(selectedMasteries).sort(),
selectedMasteryIds: Object.keys(selectedMasteries).map(function (name) {
    var cm = selectedMasteries[name];

    return cm.id || cm.cmId || cm.CmID || cm.name;
}).sort(),
        stats: totals.stats,
        spellsAdded: Object.keys(totals.spellsAdded).sort(),
        spellsRemoved: Object.keys(totals.spellsRemoved).sort()
    };
}

function publishClassMasteryState() {
    window.EQOA_CLASS_MASTERY_STATE = {
        getStatCaps: getUnlockedStatCaps,
getSelectedMasteries: function () {
    return Object.keys(selectedMasteries).map(function (name) {
        var cm = selectedMasteries[name];

        return cm.id || cm.cmId || cm.CmID || cm.name;
    }).sort();
},
        getExportData: getClassMasteryExportData
    };

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

function updatePurchasedList() {
    var list = document.getElementById('cm-purchased-list');
    var masterList = document.getElementById('cm-master-class-purchased-list');
    var names = Object.keys(selectedMasteries).sort();
    var masterNames = [];
    var purchasedNames = [];

    names.forEach(function (name) {
        if (isMasterClassTitle(selectedMasteries[name])) {
            masterNames.push(name);
        } else {
            purchasedNames.push(name);
        }
    });

    function renderRows(container, rowNames, emptyText) {
        if (!container) return;

        if (!rowNames.length) {
            container.innerHTML = emptyText;
            return;
        }

        container.innerHTML = rowNames.map(function (name) {
            var cm = selectedMasteries[name];

            return '<div class="cm-purchased-item" data-cm-name="' + escapeHtml(name) + '">' +
                '<span>' + escapeHtml(name) + '</span>' +
                '<span class="cm-purchased-cost">' + escapeHtml(cm.points) + '</span>' +
            '</div>';
        }).join('');

        Array.prototype.forEach.call(container.querySelectorAll('.cm-purchased-item'), function (row) {
            var cmName = row.getAttribute('data-cm-name');
            var cm = selectedMasteries[cmName];

            row.addEventListener('mouseenter', function () {
                showCmTooltip(cm, row);
            });

            row.addEventListener('mouseleave', hideCmTooltip);

            row.addEventListener('click', function () {
                delete selectedMasteries[cmName];
                hideCmTooltip();
                updateSummary();
            });
        });
    }

    renderRows(masterList, masterNames, 'None selected.');
    renderRows(list, purchasedNames, 'None purchased.');
}

function updateMasterClassPanel() {
    var panel = document.getElementById('cm-master-class-panel');
    var selectedTitle = getSelectedMasterClassTitle();

    if (!panel) return;

    panel.innerHTML =
        '<span class="cm-master-class-text">' +
        (selectedTitle ? escapeHtml(selectedTitle) : '-') +
        '</span>';
}

        function updateUnlockedSummary() {
            var summary = document.getElementById('cm-unlocked-summary');
            var totals = getBuildTotals();
            var html = '<div class="cm-unlocked-grid cm-unlocked-grid-three">';

            html += '<div class="cm-unlocked-box"><div class="cm-panel-title">Stats</div>';
            if (!Object.keys(totals.stats).length) {
                html += '<div>No stat changes yet.</div>';
            } else {
                Object.keys(totals.stats).sort().forEach(function (statName) {
                    var value = totals.stats[statName];
                    html += '<div class="cm-purchased-item"><span>' +
                        escapeHtml(formatStatName(statName)) +
                        '</span><span class="cm-purchased-cost">' +
                        (value > 0 ? '+' : '') +
                        escapeHtml(value) +
                        '</span></div>';
                });
            }
            html += '</div>';

            html += '<div class="cm-unlocked-box"><div class="cm-panel-title">Spells Gained</div>';
            Object.keys(totals.spellsAdded).sort().forEach(function (spell) {
                html += '<div class="cm-purchased-item">+ ' + escapeHtml(spell) + '</div>';
            });
            if (!Object.keys(totals.spellsAdded).length) html += '<div>No spells gained yet.</div>';
            html += '</div>';

            html += '<div class="cm-unlocked-box"><div class="cm-panel-title">Spells Removed</div>';
            Object.keys(totals.spellsRemoved).sort().forEach(function (spell) {
                html += '<div class="cm-purchased-item">- ' + escapeHtml(spell) + '</div>';
            });
            if (!Object.keys(totals.spellsRemoved).length) html += '<div>No spells removed yet.</div>';
            html += '</div>';

            html += '</div>';

            summary.innerHTML = html;
        }

        function updateSummary() {
            var available = getAvailablePoints();
            var spent = getSpentPoints();
            var remaining = available - spent;

            if (remaining < 0) remaining = 0;

            if (isEmbedded()) {
                updateBaseStatInputsDisplay(getBaseStatsFromInputs());
            }

            document.getElementById('cm-available').textContent = available;
            document.getElementById('cm-spent').textContent = spent;
            document.getElementById('cm-remaining').textContent = remaining;

            updatePurchasedList();
updateMasterClassPanel();
updateUnlockedSummary();
publishClassMasteryState();
renderMasteryList();
        }

        function buildTooltipHtml(cm) {
            var html = '';

            html += '<div class="cm-tooltip-title">' + escapeHtml(cm.name) + '</div>';
            html += '<div class="cm-tooltip-desc">' + escapeHtml(cm.description) + '</div>';
            html += '<div class="cm-tooltip-meta">Level ' + escapeHtml(cm.level) +
                ' | Cost ' + escapeHtml(cm.points) +
                (cm.cmSpentReq ? ' | CM Spent Req ' + escapeHtml(cm.cmSpentReq) : '') +
                '</div>';
            html += '<div class="cm-tooltip-meta">' + escapeHtml(getCmPointStatusText()) + '</div>';

            Object.keys(cm.stats || {}).forEach(function (key) {
                var value = cm.stats[key];
                var sign = value > 0 ? '+' : '';

                html += '<div class="cm-tooltip-stat">' +
                    escapeHtml(formatStatName(key)) +
                    ': ' +
                    sign +
                    escapeHtml(value) +
                    '</div>';
            });

            safeArray(cm.spellsAdded).forEach(function (spell) {
                html += '<div class="cm-tooltip-spell-add">+ ' + escapeHtml(spell) + '</div>';
            });

            safeArray(cm.spellsRemoved).forEach(function (spell) {
                html += '<div class="cm-tooltip-spell-remove">- ' + escapeHtml(spell) + '</div>';
            });

            return html;
        }

        function showCmTooltip(cm, card) {
            var rect;
            var tooltipWidth = 340;
            var leftPos;

            if (!cmTooltip || !cm) return;

            cmTooltip.innerHTML = buildTooltipHtml(cm);
            cmTooltip.style.display = 'block';

            rect = card.getBoundingClientRect();
            leftPos = rect.right + 12;

            if (leftPos + tooltipWidth > window.innerWidth) {
                leftPos = rect.left - tooltipWidth - 12;
            }

            cmTooltip.style.left = leftPos + 'px';
            cmTooltip.style.top = Math.max(10, rect.top) + 'px';
        }

        function hideCmTooltip() {
            if (cmTooltip) cmTooltip.style.display = 'none';
        }

        function renderMasteryList() {
            var masteryPanel = document.getElementById('cm-mastery-panel');
            var selectedRace = raceSelect.value;
            var selectedRaceCode = getRaceCode(selectedRace);
            var selectedClass = classSelect.value;
            var selectedPathNormalized = normalizePath(getSelectedTransformationPath());
            var selectedIsWere = isWerePath(selectedPathNormalized);
            var searchQuery = searchInput ? String(searchInput.value || '').toLowerCase() : '';
            var seen = {};
            var availableMasteries = [];
            var lockedMasteries = [];

            if (!masteryPanel) return;

            visibleMasteries = [];

            safeArray(data.masteries).forEach(function (cm) {
                var cmName = String(cm.name || '');
                var cmPath = normalizePath(cm.lycanthropy);
                var bitePath = getBitePath(cmName);
                var pathOk = false;
                var raceOk;
                var classOk;
                var tabOk;
                var searchOk;
                var lockReasons;

                raceOk = listAllowsValue(cm.races, selectedRaceCode);
                classOk = listAllowsValue(cm.classes, selectedClass);
                tabOk = String(getCmDisplayTab(cm)).toLowerCase() === String(activeCmTab).toLowerCase();
                searchOk = !searchQuery || getCmSearchText(cm).indexOf(searchQuery) !== -1;

                if (bitePath) {
                    pathOk = selectedPathNormalized === bitePath;
                } else if (!cmPath) {
                    pathOk = true;
                } else if (!selectedPathNormalized) {
                    pathOk = false;
                } else if (selectedIsWere && isSharedWereMastery(cmName)) {
                    pathOk = cmPath === 'werebeast' || isWerePath(cmPath);
                } else if (selectedIsWere && cmPath === 'werebeast') {
                    pathOk = true;
                } else {
                    pathOk = cmPath === selectedPathNormalized;
                }

                if (
                    !raceOk ||
                    !classOk ||
                    !tabOk ||
                    !pathOk ||
                    !searchOk ||
                    selectedMasteries[cmName] ||
                    seen[cmName]
                ) {
                    return;
                }

                seen[cmName] = true;

                lockReasons = getCmLockReasons(cm, false).concat(getClassPathLockReasons(cm));

                if (lockReasons.length) {
                    lockedMasteries.push({
                        cm: cm,
                        locked: true,
                        unlockHint: lockReasons.join(' | ')
                    });
                } else {
                    availableMasteries.push({
                        cm: cm,
                        locked: false,
                        unlockHint: ''
                    });
                }
            });

            visibleMasteries = showLockedCms ? availableMasteries.concat(lockedMasteries) : availableMasteries;

            if (!visibleMasteries.length) {
                masteryPanel.innerHTML =
                    'No Class Masteries found for this selection.<br>' +
                    '<small>Loaded masteries: ' + safeArray(data.masteries).length + '</small>';
                return;
            }

            masteryPanel.innerHTML = visibleMasteries.map(function (entry, index) {
                var cm = entry.cm;
                var lockedClass = entry.locked ? ' cm-card-locked' : '';
                var typeClass = getCmTypeClass(cm);
                var cmType = getCmType(cm);
var reqHtml = getRequirementsHtml(cm);
var statReqHtml = getStatReqHtml(cm);
var gainLossHtml = getCmGainLossHtml(cm);
var lockHtml = '';
var metaHtml;

                if (entry.locked) {
                    lockHtml =
                        '<div class="cm-unlock-hint">' +
                            '<strong>Locked:</strong><br>' +
                            escapeHtml(entry.unlockHint).replace(/\|/g, '<br>') +
                        '</div>';
                }

                metaHtml =
                    '<div class="cm-card-meta">' +
                        'Level: ' + escapeHtml(cm.level) +
                        ' | Cost: ' + escapeHtml(cm.points) +
                        (cm.cmSpentReq ? ' | CM Spent Req: ' + escapeHtml(cm.cmSpentReq) : '') +
                    '</div>' +
                    '<div class="cm-card-meta">' + escapeHtml(getCmPointStatusText()) + '</div>';

                if (activeCmView === 'compact') {
                    return '<div class="cm-card cm-purchasable-card cm-card-compact ' + typeClass + lockedClass + '" data-cm-index="' + index + '">' +
                        '<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
                        '<div class="cm-card-type-tag">' + escapeHtml(cmType) + '</div>' +
metaHtml +
gainLossHtml +
(statReqHtml ? '<div class="cm-card-req">Stat Req: ' + statReqHtml + '</div>' : '') +
lockHtml +
                    '</div>';
                }

                return '<div class="cm-card cm-purchasable-card ' + typeClass + lockedClass + '" data-cm-index="' + index + '">' +
                    '<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
                    '<div class="cm-card-type-tag">' + escapeHtml(cmType) + '</div>' +
'<div class="cm-card-desc">' + escapeHtml(cm.description) + '</div>' +
metaHtml +
gainLossHtml +
(reqHtml ? '<div class="cm-card-req">Requires: ' + reqHtml + '</div>' : '') +
                    (statReqHtml ? '<div class="cm-card-req">Stat Req: ' + statReqHtml + '</div>' : '') +
                    lockHtml +
                '</div>';
            }).join('');

            Array.prototype.forEach.call(masteryPanel.querySelectorAll('.cm-purchasable-card'), function (card) {
                var index = parseInt(card.getAttribute('data-cm-index'), 10);
                var entry = visibleMasteries[index];
                var cm = entry ? entry.cm : null;

          /*      card.addEventListener('mouseenter', function () {
                    showCmTooltip(cm, card);
                });

                card.addEventListener('mouseleave', hideCmTooltip); */

                card.addEventListener('click', function () {
                    var reasons;
                    var selectedTitle;

                    if (!cm || !entry) return;

                    if (entry.locked) {
                        alert(entry.unlockHint);
                        return;
                    }

                    reasons = getCmLockReasons(cm, true);

                    if (reasons.length) {
                        alert(reasons.join(' | '));
                        return;
                    }

                    if (isMasterClassTitle(cm)) {
    selectedTitle = getSelectedMasterClassTitle();

    if (selectedTitle && selectedTitle !== cm.name) {
        alert('You can only select one Master Class Title per character build.');
        return;
    }
}


var classPathReasons = getClassPathLockReasons(cm);

if (classPathReasons.length) {
    alert(classPathReasons.join(' | '));
    return;
}


selectedMasteries[cm.name] = cm;
                    hideCmTooltip();
                    updateSummary();
                });
            });
        }




        

        function setBaseStatInputs(baseStats) {
            baseStats = baseStats || {};
            document.getElementById('cm-base-str').value = numberOrZero(baseStats.STR);
            document.getElementById('cm-base-sta').value = numberOrZero(baseStats.STA);
            document.getElementById('cm-base-dex').value = numberOrZero(baseStats.DEX);
            document.getElementById('cm-base-agi').value = numberOrZero(baseStats.AGI);
            document.getElementById('cm-base-int').value = numberOrZero(baseStats.INT);
            document.getElementById('cm-base-wis').value = numberOrZero(baseStats.WIS);
            document.getElementById('cm-base-cha').value = numberOrZero(baseStats.CHA);
        }

        function loadBuildFromUrl() {
            var hash = window.location.hash || '';
            var build;

            if (hash.indexOf('#cm-build=') !== 0) return;

            try {
                build = JSON.parse(decodeURIComponent(hash.replace('#cm-build=', '')));
            } catch (e) {
                return;
            }

            if (build.r && data.raceClasses && data.raceClasses[build.r]) {
                raceSelect.value = build.r;
                loadClasses();
            }

            if (build.c) classSelect.value = build.c;
            if (build.l) levelInput.value = build.l;
            if (pathSelect && build.p !== undefined) pathSelect.value = build.p;
            if (build.b) setBaseStatInputs(build.b);

            selectedMasteries = {};

            safeArray(build.cms).forEach(function (name) {
var cm = findCmByNameOrId(name);
                if (cm) selectedMasteries[cm.name] = cm;
            });

            updateSummary();
        }

        raceSelect.addEventListener('change', loadClasses);
        classSelect.addEventListener('change', updateSummary);
        levelInput.addEventListener('input', updateSummary);

        [
            'cm-base-str',
            'cm-base-sta',
            'cm-base-dex',
            'cm-base-agi',
            'cm-base-int',
            'cm-base-wis',
            'cm-base-cha'
        ].forEach(function (id) {
            var input = document.getElementById(id);
            if (input) input.addEventListener('input', updateSummary);
        });


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

        if (clearSearchButton && searchInput) {
            clearSearchButton.addEventListener('click', function () {
                searchInput.value = '';
                renderMasteryList();
                searchInput.focus();
            });
        }

        if (showLockedInput) {
            showLockedInput.addEventListener('change', function () {
                showLockedCms = showLockedInput.checked;
                renderMasteryList();
            });
        }

        if (pathSelect) {
            pathSelect.addEventListener('change', updateSummary);
        }

        if (isEmbedded()) {
            window.addEventListener('eqoa-character-builder-state-change', syncFromCharacterBuilder);
        }

        Array.prototype.forEach.call(document.querySelectorAll('.cm-tab-button'), function (button) {
            button.addEventListener('click', function () {
                activeCmTab = button.getAttribute('data-tab');

                Array.prototype.forEach.call(document.querySelectorAll('.cm-tab-button'), function (btn) {
                    btn.classList.remove('cm-tab-active');
                });

                button.classList.add('cm-tab-active');
                updateSummary();
            });
        });

        Array.prototype.forEach.call(viewButtons, function (button) {
            button.addEventListener('click', function () {
                activeCmView = button.getAttribute('data-view') || 'detailed';

                Array.prototype.forEach.call(viewButtons, function (btn) {
                    btn.classList.remove('cm-view-active');
                });

                button.classList.add('cm-view-active');
                renderMasteryList();
            });
        });

        function resetClassMasterySelections() {
    selectedMasteries = {};
    sharedCmBuildApplied = false;
    activeCmTab = 'General';
    activeCmView = 'detailed';
    showLockedCms = true;
    levelInput.value = 60;
    setBaseStatInputs({});

    if (pathSelect) pathSelect.value = '';
    if (searchInput) searchInput.value = '';
    if (showLockedInput) showLockedInput.checked = true;

    Array.prototype.forEach.call(document.querySelectorAll('.cm-tab-button'), function (btn) {
        btn.classList.remove('cm-tab-active');
    });

    Array.prototype.forEach.call(viewButtons, function (btn) {
        btn.classList.remove('cm-view-active');
    });

    var generalTab = document.querySelector('.cm-tab-button[data-tab="General"]');
    var detailedView = document.querySelector('.cm-view-button[data-view="detailed"]');

    if (generalTab) generalTab.classList.add('cm-tab-active');
    if (detailedView) detailedView.classList.add('cm-view-active');

    hideCmTooltip();
    updateSummary();
}

window.EQOA_RESET_CLASS_MASTERY = resetClassMasterySelections;

if (resetButton) {
    resetButton.addEventListener('click', resetClassMasterySelections);
}

        loadSavedCmTheme();

        window.addEventListener('eqoa-wiki-theme-change', function (event) {
            if (event.detail && event.detail.theme) {
                applyCmTheme(event.detail.theme);
            }
        });

        loadRaces();

        if (isEmbedded()) {
            syncFromCharacterBuilder();
        } else {
            loadBuildFromUrl();
        }
    }
});