MediaWiki:ClassMasteryCalculator.js

From EQOA Sandstorm Wiki
Revision as of 23:57, 14 May 2026 by RenodorDev (talk | contribs)
Jump to navigationJump to search

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

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

    function 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 activeCmView = "detailed";
var showLockedCms = true;
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>' +

                    '<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">' +
    '<span class="cm-master-class-label">None selected</span>' +
'</div>' +

'<div class="cm-build-actions">' +
    '<button type="button" id="cm-share-url" class="cm-action-button">Generate Share URL</button>' +
    '<button type="button" id="cm-export-card" class="cm-action-button">Export Build Card</button>' +
'</div>' +

'<div id="cm-build-card-panel" class="cm-build-card-panel" style="display:none;"></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" title="Clear search">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">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');
var shareUrlButton = document.getElementById('cm-share-url');
var exportCardButton = document.getElementById('cm-export-card');
var buildCardPanel = document.getElementById('cm-build-card-panel');



function getCompressedMasteryNames(names) {
    var groups = {};
    var singles = [];

    names.forEach(function (name) {
        var base = getCmChainBase(name);
        var number = getCmChainNumber(name);

        if (number === null) {
            singles.push(name);
            return;
        }

        if (!groups[base]) {
            groups[base] = [];
        }

        groups[base].push(number);
    });

    Object.keys(groups).forEach(function (base) {

        groups[base].sort(function (a, b) {
            return a - b;
        });

        if (groups[base].length === 1) {
            singles.push(base + ' ' + groups[base][0]);
        } else {
            singles.push(base + ' ' + groups[base].join(' > '));
        }
    });

    return singles.sort();
}



        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 formatStatName(statName) {
    var map = {
        ac: 'AC',
        hp: 'HP',
        pow: 'Power',

        str: 'STR',
        sta: 'STA',
        agi: 'AGI',
        dex: 'DEX',
        wis: 'WIS',
        int: 'INT',
        cha: 'CHA',

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

        offMod: 'Offense Mod',
        defMod: 'Defense Mod',
        hasteMod: 'Haste Mod',
        regenMod: 'Regen Mod',
        powerRegenMod: 'Power Regen Mod',
        healMod: 'Healing Mod',
        spellMod: 'Spell Mod',
        meleeMod: 'Melee Mod',

        fireResist: 'Fire Resist',
        coldResist: 'Cold Resist',
        diseaseResist: 'Disease Resist',
        poisonResist: 'Poison Resist',
        magicResist: 'Magic Resist',

        fireResistMax: 'Fire Resist Max',
        coldResistMax: 'Cold Resist Max',
        diseaseResistMax: 'Disease Resist Max',
        poisonResistMax: 'Poison Resist Max',
        magicResistMax: 'Magic Resist Max'
    };

    if (map[statName]) {
        return map[statName];
    }

    return String(statName || '')
        .replace(/([a-z])([A-Z])/g, '$1 $2')
        .replace(/^./, function (letter) {
            return letter.toUpperCase();
        });
}




        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 isMasterClassTitle(cm) {
            var name = String((cm && cm.name) || '');

            return name === 'Berserker' ||
                name === 'Chosen' ||
                name === 'Deputy' ||
                name === 'Doomseeker' ||
                name === 'Immortal' ||
                name === 'Juggernaut' ||
                name === 'Mercenary' ||
                name === 'Savage' ||
                name === 'Scholar' ||
                name === 'Tinker' ||
                name === 'Arcane Were-Hunter' ||
                name === 'Divine Were-Hunter' ||
                name === 'Gypsy Were-Hunter' ||
                name === 'Heroic Were-Hunter' ||
                name === 'Metabolist' ||
                name === 'Transmuter' ||
                name === 'Gypsy' ||
                name === 'Minstrel' ||
                name === 'Exorcist' ||
                name === 'Faithful' ||
                name === 'Storm Caller' ||
                name === 'Wilding' ||
                name === 'Animator' ||
                name === 'Spellbinder' ||
                name === 'Aeromancer' ||
                name === 'Geomancer' ||
                name === 'Hydromancer' ||
                name === 'Pyromancer' ||
                name === 'Ninja' ||
                name === 'Sensei' ||
                name === 'Defiler' ||
                name === 'Lich' ||
                name === 'Crusader' ||
                name === 'Guardian' ||
                name === 'Forester' ||
                name === 'Hunter' ||
                name === 'Assassin' ||
                name === 'Burglar' ||
                name === 'Death Knight' ||
                name === 'Slayer' ||
                name === 'Elder' ||
                name === 'Mystic' ||
                name === 'Defender' ||
                name === 'Hero' ||
                name === 'Channeler' ||
                name === 'Sorcerer' ||
                name === 'Bravo' ||
                name === 'Diviner' ||
                name === 'Sage' ||
                name === 'Soldier';
        }

        function getSelectedMasterClassTitle() {
            var selectedTitle = '';

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

            return selectedTitle;
        }



function getCmSearchText(cm) {
    var parts = [];
    var stats = cm && cm.stats ? cm.stats : {};

    if (!cm) return '';

    parts.push(cm.name || '');
    parts.push(cm.description || '');
    parts.push(cm.classification || '');
    parts.push(cm.lycanthropy || '');

    Object.keys(stats).forEach(function (statName) {
        parts.push(statName);
        parts.push(stats[statName]);
    });

    safeArray(cm.spellsAdded).forEach(function (spell) {
        parts.push(spell);
    });

    safeArray(cm.spellsRemoved).forEach(function (spell) {
        parts.push(spell);
    });

    safeArray(cm.requirements).forEach(function (req) {
        parts.push(req);
    });

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



function getRequirementHtml(reqName) {
    var owned = !!selectedMasteries[reqName];
    var className = owned ? 'cm-req-owned' : 'cm-req-missing';

    return '<span class="' + className + '">' + escapeHtml(reqName) + '</span>';
}

function getRequirementsHtml(cm) {
    var reqs = safeArray(cm && cm.requirements);

    if (!reqs.length) return '';

    return reqs.map(function (reqName) {
        return getRequirementHtml(reqName);
    }).join(', ');
}



function getCmChainBase(name) {
    return String(name || '').replace(/\s+\d+$/, '');
}

function getCmChainNumber(name) {
    var match = String(name || '').match(/\s+(\d+)$/);

    return match ? parseInt(match[1], 10) : null;
}

function getCmChain(cm) {
    var base;
    var currentNumber;
    var chain = [];

    if (!cm || !cm.name) return [];

    base = getCmChainBase(cm.name);
    currentNumber = getCmChainNumber(cm.name);

    if (currentNumber === null) return [];

    safeArray(data.masteries).forEach(function (item) {
        if (
            item &&
            getCmChainBase(item.name) === base &&
            getCmChainNumber(item.name) !== null
        ) {
            chain.push(item.name);
        }
    });

    chain.sort(function (a, b) {
        return getCmChainNumber(a) - getCmChainNumber(b);
    });

    return chain;
}

function getCmChainHtml(cm) {
    var chain = getCmChain(cm);


    if (chain.length <= 1) return '';

    return chain.map(function (name) {
        var owned = !!selectedMasteries[name];
        var current = cm && cm.name === name;
        var className = 'cm-chain-step';

        if (owned) {
            className += ' cm-chain-owned';
        }

        if (current) {
            className += ' cm-chain-current';
        }

        return '<span class="' + className + '">' + escapeHtml(name) + '</span>';
    }).join('<span class="cm-chain-arrow"> &gt; </span>');
}




function getCmUnlockHint(cm) {
    var spent = getSpentPoints();
    var hints = [];

    if (!cm) return '';

    if (cm.level && cm.level > parseInt(levelInput.value, 10)) {
        hints.push('Reach level ' + cm.level);
    }

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

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

    return hints.join(' | ');
}





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

    if (!panel) return;

    if (!selectedTitle) {
        panel.innerHTML =
            '<span class="cm-master-class-label">None selected</span>';
        return;
    }

    panel.innerHTML =
        '<span class="cm-master-class-label">' + escapeHtml(selectedTitle) + '</span>' +
        '<button type="button" id="cm-remove-master-class" class="cm-mini-remove-button">Remove</button>';

    document.getElementById('cm-remove-master-class').addEventListener('click', function () {
        delete selectedMasteries[selectedTitle];
        hideCmTooltip();
        updateSummary();
    });
}



function getCmType(cm) {
    if (isMasterClassTitle(cm)) {
        return 'Master Class';
    }

    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 findCmByName(name) {
    var found = null;

    safeArray(data.masteries).forEach(function (cm) {
        if (!found && cm && cm.name === name) {
            found = cm;
        }
    });

    return found;
}

function getSelectedMasteryNames() {
    return Object.keys(selectedMasteries).filter(function (name) {
        return selectedMasteries[name];
    }).sort();
}

function copyTextToClipboard(text, button) {
    function showCopied() {
        var oldText;

        if (!button) {
            alert('Copied.');
            return;
        }

        oldText = button.textContent;
        button.textContent = 'Copied';

        setTimeout(function () {
            button.textContent = oldText;
        }, 1200);
    }

    if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(text).then(showCopied, function () {
            alert(text);
        });
        return;
    }

    alert(text);
}

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

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

        if (!cm) return;

        stats = cm.stats || {};

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

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

                statTotals[statName] += 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 buildShareUrl() {
    var build = {
        r: raceSelect.value,
        c: classSelect.value,
        l: levelInput.value,
        p: pathSelect ? pathSelect.value : '',
        cms: getSelectedMasteryNames()
    };

    return window.location.href.split('#')[0] +
        '#cm-build=' +
        encodeURIComponent(JSON.stringify(build));
}

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

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

    raw = hash.replace('#cm-build=', '');

    try {
        build = JSON.parse(decodeURIComponent(raw));
    } 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;
    }

    selectedMasteries = {};

    safeArray(build.cms).forEach(function (name) {
        var cm = findCmByName(name);

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

    updateSummary();
}

function loadQrLibrary(callback) {
    if (window.QRious) {
        callback(true);
        return;
    }

    var script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js';
    script.onload = function () {
        callback(!!window.QRious);
    };
    script.onerror = function () {
        callback(false);
    };

    document.head.appendChild(script);
}

function drawWrappedText(ctx, text, x, y, maxWidth, lineHeight, maxLines) {
    var words = String(text || '').split(' ');
    var line = '';
    var lines = [];
    var i;
    var testLine;

    for (i = 0; i < words.length; i++) {
        testLine = line + words[i] + ' ';

        if (ctx.measureText(testLine).width > maxWidth && line) {
            lines.push(line.trim());
            line = words[i] + ' ';
        } else {
            line = testLine;
        }
    }

    if (line) {
        lines.push(line.trim());
    }

    if (maxLines && lines.length > maxLines) {
        lines = lines.slice(0, maxLines);
        lines[lines.length - 1] += '...';
    }

    lines.forEach(function (lineText, index) {
        ctx.fillText(lineText, x, y + (index * lineHeight));
    });

    return y + (lines.length * lineHeight);
}

function drawPanel(ctx, x, y, w, h, title) {
    ctx.strokeStyle = '#9b7330';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.roundRect(x, y, w, h, 14);
    ctx.stroke();

    ctx.fillStyle = '#d9a64d';
    ctx.font = 'bold 32px Georgia';
    ctx.textAlign = 'center';
    ctx.fillText(title, x + w / 2, y + 42);

    ctx.strokeStyle = 'rgba(217,166,77,0.35)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(x + 28, y + 58);
    ctx.lineTo(x + w - 28, y + 58);
    ctx.stroke();

    ctx.textAlign = 'left';
}









function renderBuildCard() {
    loadQrLibrary(function (qrReady) {
        var shareUrl = buildShareUrl();
        var totals = getBuildTotals();
        var selectedNames = getSelectedMasteryNames();
        var purchasedDisplayNames = getCompressedMasteryNames(selectedNames);
        var selectedTitle = getSelectedMasterClassTitle() || 'None';
        var available = getAvailablePoints();
        var spent = getSpentPoints();
        var remaining = available - spent;
        var statsList = Object.keys(totals.stats).sort();
        var gainedList = Object.keys(totals.spellsAdded).sort();
        var removedList = Object.keys(totals.spellsRemoved).sort();

        var canvas = document.createElement('canvas');
        var ctx = canvas.getContext('2d');

        var cardWidth = 1800;
        var margin = 55;
        var panelW = cardWidth - margin * 2;
        var y = 390;

        function roundedRect(x, y, w, h, r) {
            if (ctx.roundRect) {
                ctx.beginPath();
                ctx.roundRect(x, y, w, h, r);
                return;
            }

            ctx.beginPath();
            ctx.moveTo(x + r, y);
            ctx.lineTo(x + w - r, y);
            ctx.quadraticCurveTo(x + w, y, x + w, y + r);
            ctx.lineTo(x + w, y + h - r);
            ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
            ctx.lineTo(x + r, y + h);
            ctx.quadraticCurveTo(x, y + h, x, y + h - r);
            ctx.lineTo(x, y + r);
            ctx.quadraticCurveTo(x, y, x + r, y);
        }

        function wrapLines(text, maxWidth, font) {
            var words = String(text || '').split(' ');
            var lines = [];
            var line = '';
            var test;
            var i;

            ctx.font = font;

            for (i = 0; i < words.length; i++) {
                test = line ? line + ' ' + words[i] : words[i];

                if (ctx.measureText(test).width > maxWidth && line) {
                    lines.push(line);
                    line = words[i];
                } else {
                    line = test;
                }
            }

            if (line) {
                lines.push(line);
            }

            return lines.length ? lines : [''];
        }

        function measureWrappedList(items, columns, colWidth, font, lineH, itemGap) {
            var colHeights = [];
            var i;

            for (i = 0; i < columns; i++) {
                colHeights[i] = 0;
            }

            if (!items.length) {
                return lineH;
            }

            items.forEach(function (item, index) {
                var col = index % columns;
                var lines = wrapLines(item, colWidth, font);

                colHeights[col] += lines.length * lineH + itemGap;
            });

            return Math.max.apply(null, colHeights);
        }

        function drawWrappedList(items, x, y, w, columns, color, font, lineH, itemGap) {
            var colGap = 30;
            var colWidth = (w - colGap * (columns - 1)) / columns;
            var colY = [];
            var i;

            for (i = 0; i < columns; i++) {
                colY[i] = y;
            }

            ctx.fillStyle = color;
            ctx.font = font;
            ctx.textAlign = 'left';

            if (!items.length) {
                ctx.fillText('None', x, y);
                return y + lineH;
            }

            items.forEach(function (item, index) {
                var col = index % columns;
                var drawX = x + col * (colWidth + colGap);
                var lines = wrapLines(item, colWidth, font);

                lines.forEach(function (line) {
                    ctx.fillText(line, drawX, colY[col]);
                    colY[col] += lineH;
                });

                colY[col] += itemGap;
            });

            return Math.max.apply(null, colY);
        }

        function drawPanel(x, y, w, h, title) {
            ctx.strokeStyle = '#9b7330';
            ctx.lineWidth = 3;
            roundedRect(x, y, w, h, 16);
            ctx.stroke();

            ctx.fillStyle = '#d9a64d';
            ctx.font = 'bold 36px Georgia';
            ctx.textAlign = 'center';
            ctx.fillText(title, x + w / 2, y + 48);

            ctx.strokeStyle = 'rgba(217,166,77,0.35)';
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.moveTo(x + 35, y + 68);
            ctx.lineTo(x + w - 35, y + 68);
            ctx.stroke();

            ctx.textAlign = 'left';
        }

        function drawInfoBox(label, value, x, y, w) {
            ctx.strokeStyle = '#9b7330';
            ctx.lineWidth = 2;
            roundedRect(x, y, w, 100, 12);
            ctx.stroke();

            ctx.fillStyle = '#f4e6c8';
            ctx.font = 'bold 27px Georgia';
            ctx.fillText(label, x + 28, y + 40);

            ctx.font = 'bold 31px Georgia';
            ctx.fillText(value || 'None', x + 28, y + 73);
        }

        var statsItems = statsList.map(function (statName) {
            var value = totals.stats[statName];
            var sign = value > 0 ? '+' : '';
            return formatStatName(statName) + ': ' + sign + value;
        });

        var gainedItems = gainedList.map(function (spell) {
            return '+ ' + spell;
        });

        var removedItems = removedList.map(function (spell) {
            return '- ' + spell;
        });

        var summaryH = Math.max(360, 190 + measureWrappedList(statsItems, 3, 500, 'bold 24px Georgia', 29, 2));
        var purchasedH = Math.max(360, 105 + measureWrappedList(purchasedDisplayNames, 3, 500, 'bold 24px Georgia', 29, 4));
        var spellsH = Math.max(
            360,
            180 +
                measureWrappedList(gainedItems, 3, 500, 'bold 23px Georgia', 28, 3) +
                measureWrappedList(removedItems, 3, 500, 'bold 23px Georgia', 28, 3)
        );
        var qrH = 470;

        canvas.width = cardWidth;
        canvas.height =
            390 +
            summaryH + 35 +
            purchasedH + 35 +
            spellsH + 35 +
            qrH + 120;

        ctx.fillStyle = '#05080d';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        ctx.strokeStyle = '#9b7330';
        ctx.lineWidth = 6;
        roundedRect(8, 8, canvas.width - 16, canvas.height - 16, 28);
        ctx.stroke();

        ctx.textAlign = 'center';
        ctx.fillStyle = '#d9a64d';
        ctx.font = 'bold 78px Georgia';
        ctx.fillText('EQOA: SANDSTORM', canvas.width / 2, 100);

        ctx.font = 'bold 46px Georgia';
        ctx.fillText('CLASS MASTERY CALCULATOR', canvas.width / 2, 155);

        ctx.fillStyle = '#f4e6c8';
        ctx.font = 'bold 38px Georgia';
        ctx.fillText('Unnamed CM Build', canvas.width / 2, 205);

        drawInfoBox('Race', raceSelect.value || 'None', margin, 250, 380);
        drawInfoBox('Class', classSelect.value || 'None', 480, 250, 380);
        drawInfoBox('Level', String(levelInput.value || '60'), 905, 250, 300);
        drawInfoBox('Path', (pathSelect && pathSelect.value) || 'None', 1250, 250, 495);

        drawPanel(margin, y, panelW, summaryH, 'CM SUMMARY');

        ctx.fillStyle = '#f4e6c8';
        ctx.font = 'bold 29px Georgia';
        ctx.fillText('Master Class: ' + selectedTitle, margin + 45, y + 115);
        ctx.fillText('Points: ' + spent + ' / ' + available + ' spent', margin + 45, y + 155);
        ctx.fillText('Remaining: ' + remaining, margin + 45, y + 195);

        ctx.fillStyle = '#d9a64d';
        ctx.font = 'bold 30px Georgia';
        ctx.fillText('Stats Gained', margin + 45, y + 255);

        drawWrappedList(statsItems, margin + 45, y + 295, panelW - 90, 3, '#f4e6c8', 'bold 24px Georgia', 29, 2);

        y += summaryH + 35;

        drawPanel(margin, y, panelW, purchasedH, 'PURCHASED CMS');
        drawWrappedList(purchasedDisplayNames, margin + 45, y + 110, panelW - 90, 3, '#e066ff', 'bold 24px Georgia', 29, 4);

        y += purchasedH + 35;

        drawPanel(margin, y, panelW, spellsH, 'SPELLS');

        ctx.fillStyle = '#8fd9ff';
        ctx.font = 'bold 30px Georgia';
        ctx.fillText('Gained', margin + 45, y + 115);

        var afterGainedY = drawWrappedList(gainedItems, margin + 45, y + 155, panelW - 90, 3, '#8fd9ff', 'bold 23px Georgia', 28, 3);

        ctx.fillStyle = '#ff8f8f';
        ctx.font = 'bold 30px Georgia';
        ctx.fillText('Removed', margin + 45, afterGainedY + 45);

        drawWrappedList(removedItems, margin + 45, afterGainedY + 85, panelW - 90, 3, '#ff8f8f', 'bold 23px Georgia', 28, 3);

        y += spellsH + 35;

        drawPanel(margin, y, panelW, qrH, 'SCAN TO LOAD');

        if (qrReady) {
            var qrCanvas = document.createElement('canvas');

            new QRious({
                element: qrCanvas,
                value: shareUrl,
                size: 300,
                background: 'white',
                foreground: 'black',
                level: 'M'
            });

            ctx.drawImage(qrCanvas, canvas.width / 2 - 150, y + 125, 300, 300);
        } else {
            ctx.fillStyle = '#f4e6c8';
            ctx.font = 'bold 20px Georgia';
            drawWrappedText(ctx, shareUrl, margin + 60, y + 120, panelW - 120, 26, 12);
        }

        ctx.fillStyle = '#d9a64d';
        ctx.font = 'bold 30px Georgia';
        ctx.textAlign = 'center';
        ctx.fillText(
            'EQOA: Sandstorm Wiki Class Mastery Calculator',
            canvas.width / 2,
            canvas.height - 55
        );

        var link = document.createElement('a');
        link.href = canvas.toDataURL('image/png');
        link.download = 'EQOA_CM_Build.png';
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    });
}












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

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

        if (classes.length && classes[0] !== 'All') {
            return 'Class';
        }
    }

    return classification;
}







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

        


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 updatePurchasedList() {
    var list = document.getElementById('cm-purchased-list');
    var purchasedRows = [];
    var groups = {
        'Master Class': [],
        'General': [],
        'Racial': [],
        'Archetype': [],
        'Class': [],
        'Other': []
    };
    var groupOrder = [
        'Master Class',
        'General',
        'Racial',
        'Archetype',
        'Class',
        'Other'
    ];
    var html = '';

    if (!list) return;

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

        if (!cm) return;

        if (isMasterClassTitle(cm)) {
            groupName = 'Master Class';
        } else {
            groupName = getCmDisplayTab(cm);

            if (!groups[groupName]) {
                groupName = 'Other';
            }
        }

        groups[groupName].push(cm);
    });

    groupOrder.forEach(function (groupName) {
        groups[groupName].sort(function (a, b) {
            return String(a.name || '').localeCompare(String(b.name || ''));
        });
    });

    groupOrder.forEach(function (groupName) {
        if (!groups[groupName].length) return;

        html +=
            '<div class="cm-purchased-group">' +
                '<div class="cm-purchased-group-title">' + escapeHtml(groupName) + '</div>';

        groups[groupName].forEach(function (cm) {
            var index = purchasedRows.length;
            purchasedRows.push(cm);

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

        html += '</div>';
    });

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

    list.innerHTML = html;

    Array.prototype.forEach.call(
        list.querySelectorAll('.cm-purchased-item'),
        function (row) {
            var index = parseInt(row.getAttribute('data-purchased-index'), 10);
            var cm = purchasedRows[index];

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

            row.addEventListener('mouseleave', hideCmTooltip);

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

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



       

        function updateUnlockedSummary() {
            var summary = document.getElementById('cm-unlocked-summary');
            var statTotals = {};
            var spellsAdded = {};
            var spellsRemoved = {};
            var hasStats = false;

            if (!summary) return;

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

                if (!cm) return;

                stats = cm.stats || {};

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

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

                        statTotals[statName] += value;
                        hasStats = true;
                    }
                });

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

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

            var html = '';

            html += '<div class="cm-unlocked-grid cm-unlocked-grid-three">';

            html += '<div class="cm-unlocked-box">';
            html += '<div class="cm-panel-title">Stats</div>';

            if (!hasStats) {
                html += '<div class="cm-empty-line">No stat changes yet.</div>';
            } else {
                Object.keys(statTotals).sort().forEach(function (statName) {
                    var value = statTotals[statName];
                    var sign = value > 0 ? '+' : '';

                    html +=
                        '<div class="cm-purchased-item cm-stat-row">' +
                            '<span>' + escapeHtml(formatStatName(statName)) + '</span>' +

                            '<span class="cm-purchased-cost">' + sign + escapeHtml(value) + '</span>' +
                        '</div>';
                });
            }

            html += '</div>';

            html += '<div class="cm-unlocked-box">';
            html += '<div class="cm-panel-title">Spells Gained</div>';

            if (!Object.keys(spellsAdded).length) {
                html += '<div class="cm-empty-line">No spells gained yet.</div>';
            } else {
                Object.keys(spellsAdded).sort().forEach(function (spell) {
                    html +=
                        '<div class="cm-purchased-item cm-spell-row cm-spell-gained">' +
                            '<span>+ ' + escapeHtml(spell) + '</span>' +
                        '</div>';
                });
            }

            html += '</div>';

            html += '<div class="cm-unlocked-box">';
            html += '<div class="cm-panel-title">Spells Removed</div>';

            if (!Object.keys(spellsRemoved).length) {
                html += '<div class="cm-empty-line">No spells removed yet.</div>';
            } else {
                Object.keys(spellsRemoved).sort().forEach(function (spell) {
                    html +=
                        '<div class="cm-purchased-item cm-spell-row cm-spell-removed">' +
                            '<span>- ' + escapeHtml(spell) + '</span>' +
                        '</div>';
                });
            }

            html += '</div>';
            html += '</div>';

            summary.innerHTML = html;
        }

        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();
updateMasterClassPanel();
updateUnlockedSummary();
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(formatStatName(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: ' + getRequirementsHtml(cm) +
        '</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 selectedPath = pathSelect ? pathSelect.value : '';
    var selectedPathNormalized = normalizePath(selectedPath);
    var selectedIsWere = isWerePath(selectedPathNormalized);
    var searchQuery = searchInput ? String(searchInput.value || '').toLowerCase() : '';
    var seenMasteries = {};
    var availableMasteries = [];
    var lockedMasteries = [];

    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 tabOk = getCmDisplayTab(cm) === activeCmTab;

        var 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] ||
            seenMasteries[cmName]
        ) {
            return;
        }

        seenMasteries[cmName] = true;

        var unlockHint = getCmUnlockHint(cm);

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

    if (showLockedCms) {
    visibleMasteries = availableMasteries.concat(lockedMasteries);
} else {
    visibleMasteries = availableMasteries;
}


    if (!visibleMasteries.length) {
        masteryPanel.innerHTML = 'No Class Masteries found for this selection.';
        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 requirementsHtml = getRequirementsHtml(cm);
    var chainHtml = getCmChainHtml(cm);

    var metaHtml =
        '<div class="cm-card-meta">' +
            'Level: ' + escapeHtml(cm.level) +
            ' | Cost: ' + escapeHtml(cm.points) +
            (cm.cmSpentReq ? ' | CM Spent Req: ' + escapeHtml(cm.cmSpentReq) : '') +
        '</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-compact-row">' +
                    '<div class="cm-card-title">' + escapeHtml(cm.name) + '</div>' +
                    '<div class="cm-card-type-tag">' + escapeHtml(cmType) + '</div>' +
                    metaHtml +
                '</div>' +
                (chainHtml ? '<div class="cm-chain-preview">Chain: ' + chainHtml + '</div>' : '') +
                (entry.unlockHint ? '<div class="cm-unlock-hint">' + escapeHtml(entry.unlockHint) + '</div>' : '') +
            '</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 +
            (requirementsHtml ? '<div class="cm-card-req">Requires: ' + requirementsHtml + '</div>' : '') +
            (chainHtml ? '<div class="cm-chain-preview">Chain: ' + chainHtml + '</div>' : '') +
            (entry.unlockHint ? '<div class="cm-unlock-hint">' + escapeHtml(entry.unlockHint) + '</div>' : '') +
        '</div>'
    );
}).join('');



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

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

            card.addEventListener('mouseleave', hideCmTooltip);

            card.addEventListener('click', function () {
                var available;
                var spent;
                var selectedMasterClassTitle;

                if (!cm || !entry) return;

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

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

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

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

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

                selectedMasteries[cm.name] = cm;

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


 



        raceSelect.addEventListener('change', loadClasses);
        classSelect.addEventListener('change', updateSummary);
        levelInput.addEventListener('input', updateSummary);
if (searchInput) {
    searchInput.addEventListener('input', renderMasteryList);
}

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

if (shareUrlButton) {
    shareUrlButton.addEventListener('click', function () {
        copyTextToClipboard(buildShareUrl(), shareUrlButton);
    });
}

if (exportCardButton) {
    exportCardButton.addEventListener('click', function () {
        renderBuildCard();
    });
}


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



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

        if (resetButton) {
    resetButton.addEventListener('click', function () {
        selectedMasteries = {};
        activeCmTab = 'General';
        activeCmView = 'detailed';
showLockedCms = true;

        levelInput.value = 60;

        if (pathSelect) {
            pathSelect.value = '';
        }

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

        var generalTab = document.querySelector('.cm-tab-button[data-tab="General"]');
        if (generalTab) {
            generalTab.classList.add('cm-tab-active');
        }

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

        var detailedView = document.querySelector('.cm-view-button[data-view="detailed"]');
        if (detailedView) {
            detailedView.classList.add('cm-view-active');
        }
if (searchInput) {
    searchInput.value = '';
}

if (showLockedInput) {
    showLockedInput.checked = true;
}

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


        loadRaces();
loadBuildFromUrl();

    }

});