MediaWiki:ClassMasteryCalculator.js: Difference between revisions

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


         var data = window.EQOA_CM_DATA;
         var data = window.EQOA_CM_DATA;
var selectedMasteries = {};
        var selectedMasteries = {};
var activeCmTab = "General";
        var activeCmTab = "General";


         app.innerHTML = `
         app.innerHTML = `
Line 62: Line 62:
                 <div class="character-builder-section-title" style="margin-top:18px;">Class Masteries</div>
                 <div class="character-builder-section-title" style="margin-top:18px;">Class Masteries</div>


<div class="cm-tabs">
                <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 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="Racial">Racial</button>
    <button type="button" class="cm-tab-button" data-tab="Archetype">Archetype</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>
                    <button type="button" class="cm-tab-button" data-tab="Class">Class</button>
</div>
                </div>


<div class="cm-main-layout">
                <div class="cm-main-layout">
    <div class="cm-purchased-panel">
                    <div class="cm-purchased-panel">
        <div class="cm-panel-title">CM Purchased</div>
                        <div class="cm-panel-title">CM Purchased</div>
        <div id="cm-purchased-list">None purchased.</div>
                        <div id="cm-purchased-list">None purchased.</div>
    </div>
                    </div>


    <div id="cm-mastery-panel" class="character-builder-warning-box">
                    <div id="cm-mastery-panel" class="character-builder-warning-box">
        Select a race and class to begin.
                        Select a race and class to begin.
    </div>
                    </div>
</div>
                </div>
 
<div id="cm-tooltip" class="cm-tooltip" style="display:none;"></div>


                <div id="cm-tooltip" class="cm-tooltip" style="display:none;"></div>
             </div>
             </div>
         `;
         `;
Line 88: Line 87:
         var classSelect = document.getElementById('cm-class');
         var classSelect = document.getElementById('cm-class');
         var levelInput = document.getElementById('cm-level');
         var levelInput = document.getElementById('cm-level');
var cmTooltip = document.getElementById('cm-tooltip');
        var cmTooltip = document.getElementById('cm-tooltip');


         function addOption(select, value, label) {
         function addOption(select, value, label) {
Line 135: Line 134:
         }
         }


function getSpentPoints() {
        function getSpentPoints() {
    var total = 0;
            var total = 0;


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


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


function updatePurchasedList() {
            var purchased = Object.keys(selectedMasteries).filter(function (name) {
    var list = document.getElementById('cm-purchased-list');
                return selectedMasteries[name];
    if (!list) return;
            });


    var purchased = Object.keys(selectedMasteries).filter(function (name) {
            if (!purchased.length) {
        return selectedMasteries[name];
                list.innerHTML = 'None purchased.';
    });
                return;
            }


    if (!purchased.length) {
            list.innerHTML = purchased.map(function (name) {
        list.innerHTML = 'None purchased.';
                var cm = selectedMasteries[name];
        return;
    }


    list.innerHTML = purchased.map(function (name) {
                return (
        var cm = selectedMasteries[name];
                    '<div class="cm-purchased-item" data-cm-name="' + name.replace(/"/g, '&quot;') + '">' +
                        '<span>' + name + '</span>' +
                        '<span class="cm-purchased-cost">' + cm.points + '</span>' +
                    '</div>'
                );
            }).join('');
 
            Array.prototype.forEach.call(
                list.querySelectorAll('.cm-purchased-item'),
                function (row) {
                    row.addEventListener('click', function () {
                        var cmName = row.getAttribute('data-cm-name');
                        delete selectedMasteries[cmName];
 
                        if (cmTooltip) {
                            cmTooltip.style.display = 'none';
                        }
 
                        updateSummary();
                    });
                }
            );
        }


         return (
         function updateSummary() {
             '<div class="cm-purchased-item" data-cm-name="' + name.replace(/"/g, '&quot;') + '">' +
            var available = getAvailablePoints();
                '<span>' + name + '</span>' +
             var spent = getSpentPoints();
                '<span class="cm-purchased-cost">' + cm.points + '</span>' +
            var remaining = available - spent;
             '</div>'
 
         );
            document.getElementById('cm-available').textContent = available;
    }).join('');
            document.getElementById('cm-spent').textContent = spent;
            document.getElementById('cm-remaining').textContent = remaining;
 
            updatePurchasedList();
             renderMasteryList();
        }
 
         function buildCmTooltipHtml(cm) {
            var statLines = [];
 
            Object.keys(cm.stats || {}).forEach(function (key) {
                var value = cm.stats[key];


    Array.prototype.forEach.call(
                if (value) {
        list.querySelectorAll('.cm-purchased-item'),
                    var sign = value > 0 ? '+' : '';
        function (row) {
                    statLines.push(
            row.addEventListener('click', function () {
                        '<div class="cm-tooltip-stat">' +
                var cmName = row.getAttribute('data-cm-name');
                            key + ': ' + sign + value +
                 delete selectedMasteries[cmName];
                        '</div>'
                updateSummary();
                    );
                 }
             });
             });
            var spellAdds = (cm.spellsAdded || []).map(function (spell) {
                return '<div class="cm-tooltip-spell-add">+ ' + spell + '</div>';
            }).join('');
            var spellRemoves = (cm.spellsRemoved || []).map(function (spell) {
                return '<div class="cm-tooltip-spell-remove">- ' + spell + '</div>';
            }).join('');
            var reqHtml = '';
            if ((cm.requirements || []).length) {
                reqHtml =
                    '<div class="cm-tooltip-req">' +
                        'Requires: ' + cm.requirements.join(', ') +
                    '</div>';
            }
            return (
                '<div class="cm-tooltip-title">' + cm.name + '</div>' +
                '<div class="cm-tooltip-desc">' + cm.description + '</div>' +
                '<div class="cm-tooltip-meta">' +
                    'Level ' + cm.level +
                    ' | Cost ' + cm.points +
                    (cm.cmSpentReq ? ' | CM Spent Req ' + cm.cmSpentReq : '') +
                '</div>' +
                reqHtml +
                statLines.join('') +
                spellAdds +
                spellRemoves
            );
         }
         }
    );
}


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


        function updateSummary() {
            cmTooltip.innerHTML = buildCmTooltipHtml(cm);
    var available = getAvailablePoints();
            cmTooltip.style.display = 'block';
    var spent = getSpentPoints();
    var remaining = available - spent;


    document.getElementById('cm-available').textContent = available;
            var rect = card.getBoundingClientRect();
    document.getElementById('cm-spent').textContent = spent;
    document.getElementById('cm-remaining').textContent = remaining;


    updatePurchasedList();
            cmTooltip.style.left = (rect.right + 12 + window.scrollX) + 'px';
    renderMasteryList();
            cmTooltip.style.top = (rect.top + window.scrollY) + 'px';
}
        }
 
        function hideCmTooltip() {
            if (cmTooltip) {
                cmTooltip.style.display = 'none';
            }
        }


         function renderMasteryList() {
         function renderMasteryList() {
Line 205: Line 274:
             var selectedClass = classSelect.value;
             var selectedClass = classSelect.value;
             var level = parseInt(levelInput.value, 10) || 1;
             var level = parseInt(levelInput.value, 10) || 1;
             var spent = getSpentPoints();
             var spent = getSpentPoints();


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


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


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


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


    var reqs = cm.requirements || [];
                var reqs = cm.requirements || [];


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


    var tabOk = cm.classification === activeCmTab;
                var tabOk = cm.classification === activeCmTab;


var notPurchased =
                var notPurchased =
    !selectedMasteries[cm.name];
                    !selectedMasteries[cm.name];


return raceOk && classOk && levelOk && spentOk && reqOk && tabOk && notPurchased;
                return raceOk && classOk && levelOk && spentOk && reqOk && tabOk && notPurchased;
});
            });


             if (!filtered.length) {
             if (!filtered.length) {
Line 246: Line 314:
             var html = '';
             var html = '';


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


masteryPanel.innerHTML = html;
            masteryPanel.innerHTML = html;
Array.prototype.forEach.call(
    masteryPanel.querySelectorAll('.cm-purchasable-card'),
    function (card) {
card.addEventListener('mouseenter', function (e) {


    var statLines = [];
            Array.prototype.forEach.call(
                masteryPanel.querySelectorAll('.cm-purchasable-card'),
                function (card) {
                    var cmName = card.getAttribute('data-cm-name');


    Object.keys(cm.stats || {}).forEach(function (key) {
                    var cm = (data.masteries || []).find(function (x) {
        var value = cm.stats[key];
                        return x.name === cmName;
                    });


        if (value) {
                    card.addEventListener('mouseenter', function () {
            statLines.push(
                        showCmTooltip(cm, card);
                '<div class="cm-tooltip-stat">' +
                    });
                    key + ': +' + value +
                '</div>'
            );
        }
    });
 
    var spellAdds = (cm.spellsAdded || []).map(function (spell) {
        return '<div class="cm-tooltip-spell-add">+ ' + spell + '</div>';
    }).join('');
 
    var spellRemoves = (cm.spellsRemoved || []).map(function (spell) {
        return '<div class="cm-tooltip-spell-remove">- ' + spell + '</div>';
    }).join('');
 
    var reqHtml = '';
 
    if ((cm.requirements || []).length) {
        reqHtml =
            '<div class="cm-tooltip-req">' +
                'Requires: ' + cm.requirements.join(', ') +
            '</div>';
    }
 
    cmTooltip.innerHTML =
        '<div class="cm-tooltip-title">' + cm.name + '</div>' +
        '<div class="cm-tooltip-desc">' + cm.description + '</div>' +


        '<div class="cm-tooltip-meta">' +
                    card.addEventListener('mouseleave', hideCmTooltip);
            'Level ' + cm.level +
            ' | Cost ' + cm.points +
        '</div>' +


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


        statLines.join('') +
                        var available = getAvailablePoints();
                        var spent = getSpentPoints();


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


        spellRemoves;
                        selectedMasteries[cm.name] = cm;


    cmTooltip.style.display = 'block';
                        hideCmTooltip();
 
                        updatePurchasedList();
    var rect = card.getBoundingClientRect();
                        updateSummary();
 
                    });
    cmTooltip.style.left = (rect.right + 12 + window.scrollX) + 'px';
    cmTooltip.style.top = (rect.top + window.scrollY) + 'px';
});
 
card.addEventListener('mouseleave', function () {
    cmTooltip.style.display = 'none';
});
        card.addEventListener('click', function () {
            var cmName = card.getAttribute('data-cm-name');
            var cm = (data.masteries || []).find(function (x) {
                return x.name === cmName;
            });
 
            if (!cm) return;
 
            if (selectedMasteries[cm.name]) {
                delete selectedMasteries[cm.name];
            } else {
                var available = getAvailablePoints();
                var spent = getSpentPoints();
 
                if (spent + cm.points > available) {
                    alert('Not enough CM points.');
                    return;
                 }
                 }
 
            );
                selectedMasteries[cm.name] = cm;
}
 
updatePurchasedList();
updateSummary();
        });
    }
);
         }
         }


Line 358: Line 369:
         levelInput.addEventListener('input', updateSummary);
         levelInput.addEventListener('input', updateSummary);


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


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


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


            updateSummary();
        });
    }
);
         loadRaces();
         loadRaces();
     }
     }


});
});

Revision as of 19:02, 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";

        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;">
                </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 cmTooltip = document.getElementById('cm-tooltip');

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

        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 = 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="' + name.replace(/"/g, '&quot;') + '">' +
                        '<span>' + name + '</span>' +
                        '<span class="cm-purchased-cost">' + cm.points + '</span>' +
                    '</div>'
                );
            }).join('');

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

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

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

            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">' +
                            key + ': ' + sign + value +
                        '</div>'
                    );
                }
            });

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

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

            var reqHtml = '';

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

            return (
                '<div class="cm-tooltip-title">' + cm.name + '</div>' +
                '<div class="cm-tooltip-desc">' + cm.description + '</div>' +
                '<div class="cm-tooltip-meta">' +
                    'Level ' + cm.level +
                    ' | Cost ' + cm.points +
                    (cm.cmSpentReq ? ' | CM Spent Req ' + 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();

            cmTooltip.style.left = (rect.right + 12 + window.scrollX) + 'px';
            cmTooltip.style.top = (rect.top + window.scrollY) + '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 spent = getSpentPoints();

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

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

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

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

                var reqs = cm.requirements || [];

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

                var tabOk = cm.classification === activeCmTab;

                var notPurchased =
                    !selectedMasteries[cm.name];

                return raceOk && classOk && levelOk && spentOk && reqOk && tabOk && notPurchased;
            });

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

            var html = '';

            filtered.forEach(function (cm) {
                html +=
                    '<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;

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

                    var cm = (data.masteries || []).find(function (x) {
                        return x.name === cmName;
                    });

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

        raceSelect.addEventListener('change', loadClasses);
        classSelect.addEventListener('change', updateSummary);
        levelInput.addEventListener('input', 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();
    }

});