MediaWiki:ClassMasteryCalculator.js: Difference between revisions

From EQOA Sandstorm Wiki
Jump to navigationJump to search
No edit summary
No edit summary
Line 29: Line 29:
         var selectedMasteries = {};
         var selectedMasteries = {};
         var activeCmTab = "General";
         var activeCmTab = "General";
        var visibleMasteries = [];


         app.innerHTML =
         app.innerHTML =
Line 102: Line 103:


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


         function escapeHtml(value) {
         function escapeHtml(value) {
             return String(value || '')
             return String(value == null ? '' : value)
                 .replace(/&/g, '&')
                 .replace(/&/g, '&')
                 .replace(/</g, '&lt;')
                 .replace(/</g, '&lt;')
Line 122: Line 123:


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


    var map = {
            var map = {
        lionwere: "werelion",
                lionwere: 'werelion',
        werelion: "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'
            };


        bearwere: "werebear",
            return map[value] || value;
         werebear: "werebear",
         }


         wolfwere: "werewolf",
         function getBitePath(name) {
        werewolf: "werewolf",
            name = String(name || '').toLowerCase();


        ratwere: "wererat",
            if (name.indexOf('lion bite') !== -1) return 'werelion';
        wererat: "wererat",
            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';


        gatorwere: "weregator",
            return '';
         weregator: "weregator",
         }


         vampire: "vampire",
         function isSharedWereMastery(name) {
            return name === 'Lycanthropic Resistances' ||
                name === 'Lycanthropic Regeneration' ||
                name === 'Combat Senses';
        }


        werehunter: "werehunter",
         function isWerePath(path) {
        hunterwere: "werehunter",
             return path === 'werelion' ||
 
                path === 'werebear' ||
        werebeast: "werebeast"
                path === 'werewolf' ||
    };
                 path === 'wererat' ||
 
                 path === 'weregator';
    return map[value] || value;
}
 
 
 
         function findMasteryByName(name) {
             var masteries = safeArray(data.masteries);
            var i;
 
            for (i = 0; i < masteries.length; i++) {
                 if (masteries[i].name === name) {
                    return masteries[i];
                 }
            }
 
            return null;
         }
         }


Line 231: Line 236:
             list.innerHTML = purchased.map(function (name) {
             list.innerHTML = purchased.map(function (name) {
                 var cm = selectedMasteries[name];
                 var cm = selectedMasteries[name];


                 return (
                 return (
Line 250: Line 254:
                         showCmTooltip(cm, row);
                         showCmTooltip(cm, row);
                     });
                     });


                     row.addEventListener('mouseleave', hideCmTooltip);
                     row.addEventListener('mouseleave', hideCmTooltip);
Line 277: Line 282:
         function buildCmTooltipHtml(cm) {
         function buildCmTooltipHtml(cm) {
             var statLines = [];
             var statLines = [];
            if (!cm) return '';


             Object.keys(cm.stats || {}).forEach(function (key) {
             Object.keys(cm.stats || {}).forEach(function (key) {
Line 348: Line 355:


         function renderMasteryList() {
         function renderMasteryList() {
    var masteryPanel = document.getElementById('cm-mastery-panel');
            var masteryPanel = document.getElementById('cm-mastery-panel');
    if (!masteryPanel) return;
            if (!masteryPanel) return;


    var selectedRace = raceSelect.value;
            var selectedRace = raceSelect.value;
    var selectedRaceCode = data.raceCodes[selectedRace];
            var selectedRaceCode = (data.raceCodes || {})[selectedRace];
    var selectedClass = classSelect.value;
            var selectedClass = classSelect.value;
    var level = parseInt(levelInput.value, 10) || 1;
            var level = parseInt(levelInput.value, 10) || 1;
    var selectedPath = pathSelect ? pathSelect.value : "";
            var selectedPath = pathSelect ? pathSelect.value : '';
    var selectedPathNormalized = normalizePath(selectedPath);
            var selectedPathNormalized = normalizePath(selectedPath);
    var spent = getSpentPoints();
            var selectedIsWere = isWerePath(selectedPathNormalized);
            var spent = getSpentPoints();
            var seenMasteries = {};


    var werePaths = [
            visibleMasteries = [];
        "werelion",
        "werebear",
        "werewolf",
        "wererat",
        "weregator"
    ];


    var selectedIsWere =
            safeArray(data.masteries).forEach(function (cm) {
        werePaths.indexOf(selectedPathNormalized) !== -1;
                var cmRaces = safeArray(cm.races);
                var cmClasses = safeArray(cm.classes);
                var reqs = safeArray(cm.requirements);
                var cmName = String(cm.name || '');
                var cmPath = normalizePath(cm.lycanthropy);
                var bitePath = getBitePath(cmName);
                var pathOk = false;


    var sharedWereMasteries = {
                var raceOk =
        "Lycanthropic Resistances": true,
                    !cmRaces.length ||
        "Lycanthropic Regeneration": true,
                    cmRaces.indexOf(selectedRaceCode) !== -1;
        "Combat Senses": true
    };


    var seenMasteries = {};
                var classOk =
                    !cmClasses.length ||
                    cmClasses.indexOf(selectedClass) !== -1;


    var filtered = (data.masteries || []).filter(function (cm) {
                var levelOk =
        var raceOk =
                    !cm.level ||
            !(cm.races || []).length ||
                    cm.level <= level;
            (cm.races || []).indexOf(selectedRaceCode) !== -1;


        var classOk =
                var spentOk =
            !(cm.classes || []).length ||
                    !cm.cmSpentReq ||
            (cm.classes || []).indexOf(selectedClass) !== -1;
                    spent >= cm.cmSpentReq;


        var levelOk =
                var reqOk = reqs.every(function (reqName) {
            !cm.level ||
                    return selectedMasteries[reqName];
            cm.level <= level;
                });


        var spentOk =
                var tabOk = cm.classification === activeCmTab;
            !cm.cmSpentReq ||
            spent >= cm.cmSpentReq;


        var reqs = cm.requirements || [];
                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;
                }


        var reqOk = reqs.every(function (reqName) {
                if (
            return selectedMasteries[reqName];
                    raceOk &&
        });
                    classOk &&
                    levelOk &&
                    spentOk &&
                    reqOk &&
                    tabOk &&
                    pathOk &&
                    !selectedMasteries[cmName] &&
                    !seenMasteries[cmName]
                ) {
                    seenMasteries[cmName] = true;
                    visibleMasteries.push(cm);
                }
            });


        var tabOk = cm.classification === activeCmTab;
            if (!visibleMasteries.length) {
                masteryPanel.innerHTML = 'No Class Masteries found for this selection.';
                return;
            }


        var cmPath = normalizePath(cm.lycanthropy);
            masteryPanel.innerHTML = visibleMasteries.map(function (cm, index) {
        var cmIsTransformation = cmPath !== "";
                return (
        var pathOk = false;
                    '<div class="cm-card cm-purchasable-card" data-cm-index="' + index + '">' +
                        '<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
                        '<div class="cm-card-desc">' + escapeHtml(cm.description) + '</div>' +
                        '<div class="cm-card-meta">' +
                            'Level: ' + escapeHtml(cm.level) +
                            ' | Cost: ' + escapeHtml(cm.points) +
                            (cm.cmSpentReq ? ' | CM Spent Req: ' + escapeHtml(cm.cmSpentReq) : '') +
                        '</div>' +
                    '</div>'
                );
            }).join('');


        if (!cmIsTransformation) {
            Array.prototype.forEach.call(
            pathOk = true;
                masteryPanel.querySelectorAll('.cm-purchasable-card'),
        } else if (!selectedPathNormalized) {
                function (card) {
            pathOk = false;
                    var cmIndex = parseInt(card.getAttribute('data-cm-index'), 10);
        } else if (selectedIsWere && cmPath === "werebeast") {
                    var cm = visibleMasteries[cmIndex];
            pathOk = true;
        } else if (selectedIsWere && sharedWereMasteries[cm.name] && werePaths.indexOf(cmPath) !== -1) {
            pathOk = true;
        } else {
            pathOk = cmPath === selectedPathNormalized;
        }


        var notPurchased =
                    card.addEventListener('mouseenter', function () {
            !selectedMasteries[cm.name];
                        showCmTooltip(cm, card);
                    });


        if (
                    card.addEventListener('mouseleave', hideCmTooltip);
            !raceOk ||
            !classOk ||
            !levelOk ||
            !spentOk ||
            !reqOk ||
            !tabOk ||
            !pathOk ||
            !notPurchased
        ) {
            return false;
        }


        if (seenMasteries[cm.name]) {
                    card.addEventListener('click', function () {
            return false;
                        if (!cm) return;
        }


        seenMasteries[cm.name] = true;
                        var available = getAvailablePoints();
        return true;
                        var spent = getSpentPoints();
    });


    if (!filtered.length) {
                        if (spent + cm.points > available) {
        masteryPanel.innerHTML = 'No Class Masteries found for this selection.';
                            alert('Not enough CM points.');
        return;
                            return;
    }
                        }


    var html = '';
                        selectedMasteries[cm.name] = cm;


    filtered.forEach(function (cm) {
                        hideCmTooltip();
        html +=
                        updateSummary();
            '<div class="cm-card cm-purchasable-card" data-cm-name="' + cm.name.replace(/"/g, '&quot;') + '">' +
                     });
                '<div class="cm-card-title">' + cm.name + '</div>' +
                 }
                '<div class="cm-card-desc">' + cm.description + '</div>' +
             );
                '<div class="cm-card-meta">' +
        }
                     'Level: ' + cm.level +
                    ' | Cost: ' + cm.points +
                    (cm.cmSpentReq ? ' | CM Spent Req: ' + cm.cmSpentReq : '') +
                 '</div>' +
             '</div>';
    });


    masteryPanel.innerHTML = html;
        raceSelect.addEventListener('change', loadClasses);
        classSelect.addEventListener('change', updateSummary);
        levelInput.addEventListener('input', updateSummary);


    Array.prototype.forEach.call(
         if (pathSelect) {
         masteryPanel.querySelectorAll('.cm-purchasable-card'),
             pathSelect.addEventListener('change', updateSummary);
        function (card) {
        }
             var cmName = card.getAttribute('data-cm-name');


             var cm = (data.masteries || []).find(function (x) {
        Array.prototype.forEach.call(
                return x.name === cmName;
             document.querySelectorAll('.cm-tab-button'),
            });
            function (button) {
                button.addEventListener('click', function () {
                    activeCmTab = button.getAttribute('data-tab');


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


            card.addEventListener('mouseleave', hideCmTooltip);
                    button.classList.add('cm-tab-active');


            card.addEventListener('click', function () {
                    updateSummary();
                 if (!cm) return;
                 });
            }
        );


                var available = getAvailablePoints();
        loadRaces();
                var spent = getSpentPoints();
    }
 
                if (spent + cm.points > available) {
                    alert('Not enough CM points.');
                    return;
                }
 
                selectedMasteries[cm.name] = cm;


                hideCmTooltip();
});
                updatePurchasedList();
                updateSummary();
            });
        }
    );
}

Revision as of 20:31, 14 May 2026

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

    function waitForCMData(attemptsLeft) {
        if (window.EQOA_CM_DATA) {
            initClassMasteryCalculator();
            return;
        }

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

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

    waitForCMData(30);

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

        var data = window.EQOA_CM_DATA || {};
        var selectedMasteries = {};
        var activeCmTab = "General";
        var visibleMasteries = [];

        app.innerHTML =
            '<div class="cm-calculator-box">' +
                '<div class="character-builder-title">Class Mastery Calculator</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>Transformation Path</label>' +
                    '<select id="cm-path">' +
                        '<option value="">None</option>' +
                        '<option value="Werelion">Lionwere</option>' +
                        '<option value="Werebear">Bearwere</option>' +
                        '<option value="Werewolf">Wolfwere</option>' +
                        '<option value="Wererat">Ratwere</option>' +
                        '<option value="Weregator">Gatorwere</option>' +
                        '<option value="Vampire">Vampire</option>' +
                        '<option value="Were Hunter">Were-Hunter</option>' +
                    '</select>' +
                '</div>' +

                '<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;">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-main-layout">' +
                    '<div class="cm-purchased-panel">' +
                        '<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>' +
            '</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 cmTooltip = document.getElementById('cm-tooltip');

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

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

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

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

            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 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 === 'Combat Senses';
        }

        function isWerePath(path) {
            return path === 'werelion' ||
                path === 'werebear' ||
                path === 'werewolf' ||
                path === 'wererat' ||
                path === 'weregator';
        }

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

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

            loadClasses();
        }

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

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

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

            updateSummary();
        }

        function getAvailablePoints() {
            var level = parseInt(levelInput.value, 10);

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

            levelInput.value = level;

            return level * 25;
        }

        function getSpentPoints() {
            var total = 0;

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

            return total;
        }

        function updatePurchasedList() {
            var list = document.getElementById('cm-purchased-list');
            if (!list) return;

            var purchased = Object.keys(selectedMasteries).filter(function (name) {
                return selectedMasteries[name];
            });

            if (!purchased.length) {
                list.innerHTML = 'None purchased.';
                return;
            }

            list.innerHTML = purchased.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(
                list.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();
                    });
                }
            );
        }

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

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

            updatePurchasedList();
            renderMasteryList();
        }

        function buildCmTooltipHtml(cm) {
            var statLines = [];

            if (!cm) return '';

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

                if (value) {
                    var sign = value > 0 ? '+' : '';
                    statLines.push(
                        '<div class="cm-tooltip-stat">' +
                            escapeHtml(key) + ': ' + sign + escapeHtml(value) +
                        '</div>'
                    );
                }
            });

            var spellAdds = safeArray(cm.spellsAdded).map(function (spell) {
                return '<div class="cm-tooltip-spell-add">+ ' + escapeHtml(spell) + '</div>';
            }).join('');

            var spellRemoves = safeArray(cm.spellsRemoved).map(function (spell) {
                return '<div class="cm-tooltip-spell-remove">- ' + escapeHtml(spell) + '</div>';
            }).join('');

            var reqHtml = '';

            if (safeArray(cm.requirements).length) {
                reqHtml =
                    '<div class="cm-tooltip-req">' +
                        'Requires: ' + escapeHtml(safeArray(cm.requirements).join(', ')) +
                    '</div>';
            }

            return (
                '<div class="cm-tooltip-title">' + escapeHtml(cm.name) + '</div>' +
                '<div class="cm-tooltip-desc">' + escapeHtml(cm.description) + '</div>' +
                '<div class="cm-tooltip-meta">' +
                    'Level ' + escapeHtml(cm.level) +
                    ' | Cost ' + escapeHtml(cm.points) +
                    (cm.cmSpentReq ? ' | CM Spent Req ' + escapeHtml(cm.cmSpentReq) : '') +
                '</div>' +
                reqHtml +
                statLines.join('') +
                spellAdds +
                spellRemoves
            );
        }

        function showCmTooltip(cm, card) {
            if (!cmTooltip || !cm) return;

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

            var rect = card.getBoundingClientRect();
            var tooltipWidth = 340;
            var 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');
            if (!masteryPanel) return;

            var selectedRace = raceSelect.value;
            var selectedRaceCode = (data.raceCodes || {})[selectedRace];
            var selectedClass = classSelect.value;
            var level = parseInt(levelInput.value, 10) || 1;
            var selectedPath = pathSelect ? pathSelect.value : '';
            var selectedPathNormalized = normalizePath(selectedPath);
            var selectedIsWere = isWerePath(selectedPathNormalized);
            var spent = getSpentPoints();
            var seenMasteries = {};

            visibleMasteries = [];

            safeArray(data.masteries).forEach(function (cm) {
                var cmRaces = safeArray(cm.races);
                var cmClasses = safeArray(cm.classes);
                var reqs = safeArray(cm.requirements);
                var cmName = String(cm.name || '');
                var cmPath = normalizePath(cm.lycanthropy);
                var bitePath = getBitePath(cmName);
                var pathOk = false;

                var raceOk =
                    !cmRaces.length ||
                    cmRaces.indexOf(selectedRaceCode) !== -1;

                var classOk =
                    !cmClasses.length ||
                    cmClasses.indexOf(selectedClass) !== -1;

                var levelOk =
                    !cm.level ||
                    cm.level <= level;

                var spentOk =
                    !cm.cmSpentReq ||
                    spent >= cm.cmSpentReq;

                var reqOk = reqs.every(function (reqName) {
                    return selectedMasteries[reqName];
                });

                var tabOk = cm.classification === activeCmTab;

                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 &&
                    levelOk &&
                    spentOk &&
                    reqOk &&
                    tabOk &&
                    pathOk &&
                    !selectedMasteries[cmName] &&
                    !seenMasteries[cmName]
                ) {
                    seenMasteries[cmName] = true;
                    visibleMasteries.push(cm);
                }
            });

            if (!visibleMasteries.length) {
                masteryPanel.innerHTML = 'No Class Masteries found for this selection.';
                return;
            }

            masteryPanel.innerHTML = visibleMasteries.map(function (cm, index) {
                return (
                    '<div class="cm-card cm-purchasable-card" data-cm-index="' + index + '">' +
                        '<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
                        '<div class="cm-card-desc">' + escapeHtml(cm.description) + '</div>' +
                        '<div class="cm-card-meta">' +
                            'Level: ' + escapeHtml(cm.level) +
                            ' | Cost: ' + escapeHtml(cm.points) +
                            (cm.cmSpentReq ? ' | CM Spent Req: ' + escapeHtml(cm.cmSpentReq) : '') +
                        '</div>' +
                    '</div>'
                );
            }).join('');

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

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

                    card.addEventListener('mouseleave', hideCmTooltip);

                    card.addEventListener('click', function () {
                        if (!cm) return;

                        var available = getAvailablePoints();
                        var spent = getSpentPoints();

                        if (spent + cm.points > available) {
                            alert('Not enough CM points.');
                            return;
                        }

                        selectedMasteries[cm.name] = cm;

                        hideCmTooltip();
                        updateSummary();
                    });
                }
            );
        }

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

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

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

        loadRaces();
    }

});