MediaWiki:AchievementTracker.js

From EQOA Sandstorm Wiki
Revision as of 03:47, 16 July 2026 by DevRenodor (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.
function initEqoaItemAutoDrops(root) {
    root = root && root.jquery ? root[0] : (root || document);

    var droppedBox = root.querySelector('.eqoa-auto-droppedby');
    var zoneBox = root.querySelector('.eqoa-auto-zone');

    if (!droppedBox || !zoneBox || droppedBox.classList.contains('eqoa-auto-drop-ready')) return;

    droppedBox.classList.add('eqoa-auto-drop-ready');

    var itemName =
        (root.querySelector('.eqoa-misc-inspect-name') && root.querySelector('.eqoa-misc-inspect-name').textContent.trim()) ||
        (root.querySelector('.item-header') && root.querySelector('.item-header').textContent.trim()) ||
        mw.config.get('wgTitle');

    if (!itemName) return;

    function cleanText(text) {
        return String(text || '').replace(/\s+/g, ' ').trim();
    }

    function norm(text) {
        return cleanText(text).toLowerCase();
    }

    function esc(text) {
        return String(text || '').replace(/[&<>"']/g, function (ch) {
            return {
                '&': '&amp;',
                '<': '&lt;',
                '>': '&gt;',
                '"': '&quot;',
                "'": '&#039;'
            }[ch];
        });
    }

    function pageLink(title) {
        return '<a href="' + mw.util.getUrl(title) + '">' + esc(title) + '</a>';
    }

    function shouldReplace(box) {
        var text = norm(box.textContent);
        return !text || text === 'loading...' || text === 'unknown' || text === 'none';
    }

    var api = new mw.Api();
    var foundDrops = [];
    var foundZones = {};
    var wanted = norm(itemName);

    api.get({
        action: 'query',
        generator: 'backlinks',
        gbltitle: itemName,
        gbllimit: 'max',
        prop: 'info',
        format: 'json'
    }).then(function (data) {
        var pages = data.query && data.query.pages ? Object.values(data.query.pages) : [];

        pages = pages.filter(function (page) {
            return page.ns === 0;
        });

        if (!pages.length) {
            if (shouldReplace(droppedBox)) droppedBox.textContent = 'Unknown';
            if (shouldReplace(zoneBox)) zoneBox.textContent = 'Unknown';
            return;
        }

        return Promise.all(pages.map(function (page) {
            return api.get({
                action: 'parse',
                page: page.title,
                prop: 'text',
                format: 'json'
            }).then(function (parsed) {
                var html = parsed.parse && parsed.parse.text ? parsed.parse.text['*'] : '';
                var doc = new DOMParser().parseFromString(html, 'text/html');
                var rows = doc.querySelectorAll('.eqoa-npc-zone-table tr');

                rows.forEach(function (row) {
                    var cells = row.querySelectorAll('td');

                    if (cells.length < 7) return;

                    var zone = cleanText(cells[0].textContent);
                    var level = cleanText(cells[1].textContent);
                    var dropsText = norm(cells[3].textContent);

                    if (!zone || dropsText.indexOf(wanted) === -1) return;

                    foundZones[zone] = true;

                    foundDrops.push({
                        npc: page.title,
                        zone: zone,
                        level: level
                    });
                });
            }).catch(function () {});
        })).then(function () {
            var dropHtml;
            var zoneHtml;

            if (foundDrops.length) {
                dropHtml = foundDrops.map(function (drop) {
                    var levelText = drop.level ? ' <span class="eqoa-auto-drop-level">(Lv ' + esc(drop.level) + ')</span>' : '';
                    return pageLink(drop.npc) + levelText;
                }).join('<br>');

                zoneHtml = Object.keys(foundZones).sort().map(pageLink).join('<br>');
            } else {
                dropHtml = 'Unknown';
                zoneHtml = 'Unknown';
            }

            if (shouldReplace(droppedBox)) droppedBox.innerHTML = dropHtml;
            if (shouldReplace(zoneBox)) zoneBox.innerHTML = zoneHtml;
        });
    });
}

mw.hook('wikipage.content').add(initEqoaItemAutoDrops);

document.addEventListener('DOMContentLoaded', function () {
    initEqoaItemAutoDrops(document);
});




function initEqoaItemAutoMerchantsAndQuests(root) {
    root = root && root.jquery ? root[0] : (root || document);

    var merchantBox = root.querySelector('.eqoa-auto-merchants');
    var questBox = root.querySelector('.eqoa-auto-quests');

    if ((!merchantBox && !questBox) || root.querySelector('.eqoa-auto-item-links-ready')) return;

    var marker = document.createElement('span');
    marker.className = 'eqoa-auto-item-links-ready';
    marker.style.display = 'none';
    root.appendChild(marker);

    var itemName =
        (root.querySelector('.eqoa-misc-inspect-name') && root.querySelector('.eqoa-misc-inspect-name').textContent.trim()) ||
        (root.querySelector('.eqoa-itembox-name') && root.querySelector('.eqoa-itembox-name').textContent.trim()) ||
        mw.config.get('wgTitle');

    if (!itemName) return;

    function cleanText(text) {
        return String(text || '').replace(/\s+/g, ' ').trim();
    }

    function norm(text) {
        return cleanText(text).toLowerCase();
    }

    function esc(text) {
        return String(text || '').replace(/[&<>"']/g, function (ch) {
            return {
                '&': '&amp;',
                '<': '&lt;',
                '>': '&gt;',
                '"': '&quot;',
                "'": '&#039;'
            }[ch];
        });
    }

    function pageLink(title) {
        return '<a href="' + mw.util.getUrl(title) + '">' + esc(title) + '</a>';
    }

    function shouldReplace(box) {
        if (!box) return false;

        var text = norm(box.textContent);
        return !text || text === 'loading...' || text === 'unknown' || text === 'none';
    }

    var wanted = norm(itemName);
    var api = new mw.Api();
    var merchants = {};
    var quests = {};

    api.get({
        action: 'query',
        generator: 'backlinks',
        gbltitle: itemName,
        gbllimit: 'max',
        prop: 'info',
        format: 'json'
    }).then(function (data) {
        var pages = data.query && data.query.pages ? Object.values(data.query.pages) : [];

        pages = pages.filter(function (page) {
            return page.ns === 0 && page.title !== mw.config.get('wgTitle');
        });

        if (!pages.length) {
            if (shouldReplace(merchantBox)) merchantBox.textContent = 'None';
            if (shouldReplace(questBox)) questBox.textContent = 'None';
            return;
        }

        return Promise.all(pages.map(function (page) {
            return api.get({
                action: 'parse',
                page: page.title,
                prop: 'text',
                format: 'json'
            }).then(function (parsed) {
                var html = parsed.parse && parsed.parse.text ? parsed.parse.text['*'] : '';
                var doc = new DOMParser().parseFromString(html, 'text/html');

                var sellsBox = doc.querySelector('.eqoa-merchant-sells');
                if (sellsBox && norm(sellsBox.textContent).indexOf(wanted) !== -1) {
                    merchants[page.title] = true;
                }

                var questPage = doc.querySelector('.quest-template');
                if (questPage && norm(doc.body.textContent).indexOf(wanted) !== -1) {
                    quests[page.title] = true;
                }
            }).catch(function () {});
        })).then(function () {
            var merchantTitles = Object.keys(merchants).sort();
            var questTitles = Object.keys(quests).sort();

            if (shouldReplace(merchantBox)) {
                merchantBox.innerHTML = merchantTitles.length
                    ? merchantTitles.map(pageLink).join('<br>')
                    : 'None';
            }

            if (shouldReplace(questBox)) {
                questBox.innerHTML = questTitles.length
                    ? questTitles.map(pageLink).join('<br>')
                    : 'None';
            }
        });
    });
}

mw.hook('wikipage.content').add(initEqoaItemAutoMerchantsAndQuests);

document.addEventListener('DOMContentLoaded', function () {
    initEqoaItemAutoMerchantsAndQuests(document);
});





function initZoneDropsFromNpcPages(root) {
    root = root && root.jquery ? root[0] : (root || document);

    var zoneName = String(mw.config.get('wgTitle') || '').replace(/_/g, ' ');
    var dropsPanel = root.querySelector('[data-tab-panel="items"], [data-tab-panel="drops"]');

    if (!zoneName || !dropsPanel || dropsPanel.classList.contains('eqoa-zone-drops-from-npcs-ready')) {
        return;
    }

    dropsPanel.classList.add('eqoa-zone-drops-from-npcs-ready');

    var npcCategory = zoneName + ' NPCs';
    var api = new mw.Api();

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

    function normalizePageName(value) {
        return String(value || '')
            .replace(/_/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();
    }

    function escapeHtml(value) {
        return String(value || '').replace(/[&<>"']/g, function (char) {
            return {
                '&': '&amp;',
                '<': '&lt;',
                '>': '&gt;',
                '"': '&quot;',
                "'": '&#039;'
            }[char];
        });
    }

    function getGeneratedItemByName(name) {
        var wanted = normalizeText(name);
        var items = window.EQOA_GENERATED_ITEMS || [];

        for (var i = 0; i < items.length; i++) {
            if (normalizeText(items[i].name) === wanted) {
                return items[i];
            }
        }

        return null;
    }

    function isGearItem(item) {
        if (!item) return false;

        return !!(
            item.slot ||
            item.lvl ||
            item.level ||
            item.class ||
            item.dmg ||
            item.ac ||
            item.dur ||
            item.gearhp
        );
    }

    function formatItemClass(item) {
        if (!item || !item.class) return '';

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

        return String(item.class || '');
    }

    function getItemLevel(item) {
        if (!item) return '';

        return item.lvl || item.level || '';
    }

    function parseNpcPageForDrops(html, npcName) {
        var doc = new DOMParser().parseFromString(html, 'text/html');
        var rows = doc.querySelectorAll('.eqoa-npc-zone-table tr');
        var results = [];

        rows.forEach(function (row) {
            var cells = row.querySelectorAll('td');

            if (cells.length < 4) return;

            var rowZone = normalizeText(cells[0].textContent);

            if (rowZone !== normalizeText(zoneName)) {
                return;
            }

            cells[3].querySelectorAll('a').forEach(function (link) {
                var itemName = normalizePageName(link.textContent);

                if (!itemName) return;

                results.push({
                    name: itemName,
                    npc: npcName
                });
            });
        });

        return results;
    }

    function renderDropsTable(drops) {
        var seen = {};
        var uniqueDrops = [];

        drops.forEach(function (drop) {
            var key = normalizeText(drop.name);

            if (seen[key]) return;

            seen[key] = true;
            uniqueDrops.push(drop);
        });

        uniqueDrops.sort(function (a, b) {
            return a.name.localeCompare(b.name);
        });

        var html = ''
            + '<div class="zone-drops-auto-wrapper">'
            + '<table id="items-found-here-table" class="wikitable sortable zone-data-table zone-drops-auto-table">'
            + '<thead>'
            + '<tr>'
            + '<th>Name</th>'
            + '<th>Level</th>'
            + '<th>Class</th>'
            + '</tr>'
            + '</thead>'
            + '<tbody>';

        if (!uniqueDrops.length) {
            html += '<tr><td colspan="3">No drops found.</td></tr>';
        } else {
            uniqueDrops.forEach(function (drop) {
                var item = getGeneratedItemByName(drop.name);
                var gearItem = isGearItem(item);
                var level = gearItem ? getItemLevel(item) : '';
                var itemClass = gearItem ? formatItemClass(item) : '';
                var itemUrl = mw.util.getUrl(drop.name);

                html += ''
                    + '<tr>'
                    + '<td>'
                    + '<a class="class-item-tooltip-link" data-class-item-name="' + encodeURIComponent(drop.name) + '" href="' + itemUrl + '">'
                    + escapeHtml(drop.name)
                    + '</a>'
                    + '</td>'
                    + '<td>' + escapeHtml(level) + '</td>'
                    + '<td>' + escapeHtml(itemClass) + '</td>'
                    + '</tr>';
            });
        }

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

        dropsPanel.innerHTML = html;

        if (typeof initGeneratedItemTooltipsForWiki === 'function') {
            initGeneratedItemTooltipsForWiki(dropsPanel);
        }

        if (typeof applyClassItemRarityStyle === 'function') {
            dropsPanel.querySelectorAll('.class-item-tooltip-link').forEach(function (link) {
                var itemName = decodeURIComponent(link.getAttribute('data-class-item-name') || link.textContent || '');
                var item = getGeneratedItemByName(itemName);

                if (item) {
                    applyClassItemRarityStyle(link, item);
                }
            });
        }

        if (typeof initItemsFoundHereSortControls === 'function') {
            initItemsFoundHereSortControls(dropsPanel);
        }

        if (window.jQuery && jQuery.fn.tablesorter) {
            jQuery(dropsPanel).find('table.sortable').tablesorter();
        }
    }

    api.get({
        action: 'query',
        list: 'categorymembers',
        cmtitle: 'Category:' + npcCategory,
        cmlimit: '500',
        format: 'json'
    }).then(function (data) {
        var members = (data.query && data.query.categorymembers) || [];

        if (!members.length) {
            renderDropsTable([]);
            return;
        }

        return Promise.all(members.map(function (member) {
            return api.get({
                action: 'parse',
                page: member.title,
                prop: 'text',
                format: 'json'
            }).then(function (pageData) {
                var html = pageData.parse && pageData.parse.text && pageData.parse.text['*'];

                if (!html) return [];

                return parseNpcPageForDrops(html, member.title);
            }).catch(function () {
                return [];
            });
        }));
    }).then(function (dropGroups) {
        if (!dropGroups) return;

        renderDropsTable([].concat.apply([], dropGroups));
    }).catch(function () {
        renderDropsTable([]);
    });
}

mw.hook('wikipage.content').add(initZoneDropsFromNpcPages);

document.addEventListener('DOMContentLoaded', function () {
    initZoneDropsFromNpcPages(document);
});

setTimeout(function () {
    initZoneDropsFromNpcPages(document);
}, 500);

setTimeout(function () {
    initZoneDropsFromNpcPages(document);
}, 1500);



function initEqoaScreenshotLightbox(root) {
    root = root && root.jquery ? root[0] : (root || document);

    var cards = Array.from(root.querySelectorAll('.eqoa-zone-screenshot-card'));
    if (!cards.length) return;

    var images = cards.map(function (card) {
        return card.querySelector('img');
    }).filter(Boolean);

    if (!images.length) return;

    function getFullImageUrl(img) {
        var src = img.currentSrc || img.src || '';
        var match = src.match(/^(.*\/images\/)thumb\/(.+?)\/[^\/?#]+(\?.*)?$/);

        if (match) {
            return match[1] + match[2];
        }

        return src;
    }

    function getLightbox() {
        var box = document.querySelector('.eqoa-screenshot-lightbox');

        if (!box) {
            box = document.createElement('div');
            box.className = 'eqoa-screenshot-lightbox';
            box.innerHTML =
                '<button type="button" class="eqoa-screenshot-lightbox-close">×</button>' +
                '<button type="button" class="eqoa-screenshot-lightbox-prev">‹</button>' +
                '<img class="eqoa-screenshot-lightbox-image" alt="Screenshot">' +
                '<button type="button" class="eqoa-screenshot-lightbox-next">›</button>' +
                '<div class="eqoa-screenshot-lightbox-counter"></div>';

            document.body.appendChild(box);
        }

        return box;
    }

    var currentIndex = 0;

    function showImage(index) {
        var box = getLightbox();
        var img = box.querySelector('.eqoa-screenshot-lightbox-image');
        var counter = box.querySelector('.eqoa-screenshot-lightbox-counter');

        currentIndex = (index + images.length) % images.length;

        img.src = getFullImageUrl(images[currentIndex]);
        counter.textContent = 'Screenshot ' + (currentIndex + 1) + ' / ' + images.length;

        box.classList.add('is-open');
        document.body.classList.add('eqoa-screenshot-lightbox-open');
    }

    function closeLightbox() {
        var box = getLightbox();

        box.classList.remove('is-open');
        document.body.classList.remove('eqoa-screenshot-lightbox-open');
    }

    function nextImage() {
        showImage(currentIndex + 1);
    }

    function prevImage() {
        showImage(currentIndex - 1);
    }

    images.forEach(function (img, index) {
        if (img.classList.contains('eqoa-screenshot-lightbox-ready')) return;

        img.classList.add('eqoa-screenshot-lightbox-ready');
        img.addEventListener('click', function (event) {
            event.preventDefault();
            showImage(index);
        });
    });

    var box = getLightbox();

    box.querySelector('.eqoa-screenshot-lightbox-close').onclick = closeLightbox;
    box.querySelector('.eqoa-screenshot-lightbox-next').onclick = nextImage;
    box.querySelector('.eqoa-screenshot-lightbox-prev').onclick = prevImage;

    box.addEventListener('click', function (event) {
        if (event.target === box) {
            closeLightbox();
        }
    });

    document.addEventListener('keydown', function (event) {
        if (!box.classList.contains('is-open')) return;

        if (event.key === 'Escape') closeLightbox();
        if (event.key === 'ArrowRight') nextImage();
        if (event.key === 'ArrowLeft') prevImage();
    });
}

mw.hook('wikipage.content').add(initEqoaScreenshotLightbox);

document.addEventListener('DOMContentLoaded', function () {
    initEqoaScreenshotLightbox(document);
});



/* Add Class under Race / Transformation on Ultimate Build output */
(function () {
    var CLASS_NAMES = {
        ALC: 'Alchemist',
        BRD: 'Bard',
        CL: 'Cleric',
        DRD: 'Druid',
        ENC: 'Enchanter',
        MAG: 'Magician',
        MNK: 'Monk',
        NEC: 'Necromancer',
        PAL: 'Paladin',
        RAN: 'Ranger',
        RGE: 'Rogue',
        ROG: 'Rogue',
        SK: 'Shadowknight',
        SHA: 'Shaman',
        WAR: 'Warrior',
        WIZ: 'Wizard'
    };

    function cleanText(value) {
        return String(value || '').replace(/\s+/g, ' ').trim();
    }

    function normalizeClassName(value) {
        var text = cleanText(value).replace(/^Class:\s*/i, '');
        var upper = text.toUpperCase();

        if (CLASS_NAMES[upper]) {
            return CLASS_NAMES[upper];
        }

        return Object.keys(CLASS_NAMES).map(function (key) {
            return CLASS_NAMES[key];
        }).find(function (name) {
            return name.toLowerCase() === text.toLowerCase();
        }) || text;
    }

    function getSelectedClassName() {
        var globals = [
            window.CB_SELECTED_CLASS,
            window.selectedClass,
            window.currentClass,
            window.eqoaSelectedClass
        ];

        var found;
        var selects;
        var active;
        var checked;

        found = globals.map(normalizeClassName).find(Boolean);
        if (found) return found;

        selects = document.querySelectorAll('select');
        found = Array.prototype.slice.call(selects).map(function (select) {
            var option = select.options[select.selectedIndex];
            return normalizeClassName((option && option.textContent) || select.value);
        }).find(function (value) {
            return Object.keys(CLASS_NAMES).map(function (key) {
                return CLASS_NAMES[key];
            }).indexOf(value) !== -1;
        });

        if (found) return found;

        active = document.querySelectorAll(
            '[data-class].is-active, [data-class].active, [data-class].selected,' +
            '[data-cb-class].is-active, [data-cb-class].active, [data-cb-class].selected,' +
            '[data-player-class].is-active, [data-player-class].active, [data-player-class].selected'
        );

        found = Array.prototype.slice.call(active).map(function (node) {
            return normalizeClassName(
                node.getAttribute('data-class') ||
                node.getAttribute('data-cb-class') ||
                node.getAttribute('data-player-class') ||
                node.textContent
            );
        }).find(Boolean);

        if (found) return found;

        checked = document.querySelectorAll('input[type="radio"]:checked, input[type="checkbox"]:checked');
        found = Array.prototype.slice.call(checked).map(function (input) {
            if (!/class/i.test(input.name + ' ' + input.id + ' ' + input.className)) return '';
            return normalizeClassName(input.value);
        }).find(Boolean);

        return found || '';
    }

    function findRaceLabel(root) {
        return Array.prototype.slice.call((root || document).querySelectorAll('*')).find(function (node) {
            return cleanText(node.textContent) === 'Race / Transformation' && !node.children.length;
        });
    }

    function findSmallBlock(label) {
        var current = label.parentElement;
        var best = label.parentElement;

        while (current && current !== document.body) {
            if (
                cleanText(current.textContent).indexOf('Race / Transformation') !== -1 &&
                cleanText(current.textContent).length <= 160
            ) {
                best = current;
            }

            current = current.parentElement;
        }

        return best;
    }

    function writeClassBlock(block, className) {
        var walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
        var textNodes = [];
        var node;

        while ((node = walker.nextNode())) {
            if (cleanText(node.nodeValue)) {
                textNodes.push(node);
            }
        }

        if (textNodes[0]) {
            textNodes[0].nodeValue = textNodes[0].nodeValue.replace(cleanText(textNodes[0].nodeValue), 'Class');
        }

        if (textNodes[1]) {
            textNodes[1].nodeValue = textNodes[1].nodeValue.replace(cleanText(textNodes[1].nodeValue), className);
        } else {
            block.appendChild(document.createTextNode(className));
        }
    }

    function addUltimateBuildClass(root) {
        var className = getSelectedClassName();
        var raceLabel = findRaceLabel(root);
        var raceBlock;
        var parent;
        var existing;
        var classBlock;

        if (!className || !raceLabel) return;

        raceBlock = findSmallBlock(raceLabel);
        parent = raceBlock.parentNode;

        existing = Array.prototype.slice.call(parent.children).find(function (child) {
            return child.classList && child.classList.contains('cb-ultimate-class-injected');
        });

        classBlock = existing || raceBlock.cloneNode(true);
        classBlock.classList.add('cb-ultimate-class-injected');

        writeClassBlock(classBlock, className);

        if (!existing) {
            parent.insertBefore(classBlock, raceBlock.nextSibling);
        }
    }

    function runUltimateBuildClassUpdate() {
        addUltimateBuildClass(document);
    }

    if (window.mw && mw.hook) {
        mw.hook('wikipage.content').add(function ($content) {
            addUltimateBuildClass($content && $content[0] ? $content[0] : document);
        });
    }

    document.addEventListener('DOMContentLoaded', runUltimateBuildClassUpdate);
    document.addEventListener('change', function () {
        setTimeout(runUltimateBuildClassUpdate, 50);
    });
    document.addEventListener('click', function () {
        setTimeout(runUltimateBuildClassUpdate, 50);
    });

    setTimeout(runUltimateBuildClassUpdate, 500);
    setTimeout(runUltimateBuildClassUpdate, 1500);
}());




/* Zone NPC tab: use NPCFriendly/NPCMerchant |race= as Family and |level= as Level when blank */
(function () {
    var npcFriendlyDataCache = {};
    var observerStarted = false;

    function normalizeTitleFromRow(nameCell) {
        var link = nameCell ? nameCell.querySelector('a') : null;
        var href;
        var match;

        if (link) {
            href = link.getAttribute('href') || '';

            match = href.match(/[?&]title=([^&]+)/);
            if (match) return decodeURIComponent(match[1]).replace(/_/g, ' ');

            match = href.match(/\/wiki\/([^?#]+)/);
            if (match) return decodeURIComponent(match[1]).replace(/_/g, ' ');

            return link.textContent.replace(/\s+/g, ' ').trim();
        }

        return nameCell ? nameCell.textContent.replace(/\s+/g, ' ').trim() : '';
    }

    function extractTemplateParam(source, templateName, paramName) {
        var start = source.search(new RegExp('{{\\s*' + templateName + '\\b', 'i'));
        var chunk;
        var match;

        if (start === -1) return '';

        chunk = source.slice(start);

        match = chunk.match(new RegExp(
            '(?:^|\\n)\\s*\\|\\s*' + paramName + '\\s*=\\s*([\\s\\S]*?)(?=\\n\\s*\\|\\s*[A-Za-z0-9_ -]+\\s*=|\\n\\s*}}|$)',
            'i'
        ));

        return match ? match[1].trim() : '';
    }

    function getNpcFriendlyTemplateData(source) {
        var templateName = '';
        var race = '';
        var level = '';

        if (/{{\s*NPCFriendly\b/i.test(source)) {
            templateName = 'NPCFriendly';
        } else if (/{{\s*NPCMerchant\b/i.test(source)) {
            templateName = 'NPCMerchant';
        }

        if (!templateName) {
            return {
                race: '',
                level: ''
            };
        }

        race = extractTemplateParam(source, templateName, 'race');
        level = extractTemplateParam(source, templateName, 'level') || extractTemplateParam(source, templateName, 'lvl');

        return {
            race: race,
            level: level
        };
    }

    function parseWikiText(api, text) {
        if (!text) return Promise.resolve('');

        return api.get({
            action: 'parse',
            text: text,
            contentmodel: 'wikitext',
            prop: 'text',
            formatversion: 2
        }).then(function (data) {
            return data && data.parse && data.parse.text ? data.parse.text : text;
        }).catch(function () {
            return text;
        });
    }

    function loadNpcFriendlyData(title) {
        var api;

        if (!title) {
            return Promise.resolve({
                raceHtml: '',
                levelHtml: ''
            });
        }

        if (npcFriendlyDataCache[title]) {
            return npcFriendlyDataCache[title];
        }

        api = new mw.Api();

        npcFriendlyDataCache[title] = api.get({
            action: 'query',
            prop: 'revisions',
            titles: title,
            rvprop: 'content',
            rvslots: 'main',
            formatversion: 2
        }).then(function (data) {
            var page = data.query && data.query.pages && data.query.pages[0];
            var source = page && page.revisions && page.revisions[0] && page.revisions[0].slots.main.content;
            var templateData;

            if (!source) {
                return {
                    raceHtml: '',
                    levelHtml: ''
                };
            }

            templateData = getNpcFriendlyTemplateData(source);

            return Promise.all([
                parseWikiText(api, templateData.race),
                parseWikiText(api, templateData.level)
            ]).then(function (parsed) {
                return {
                    raceHtml: parsed[0] || '',
                    levelHtml: parsed[1] || ''
                };
            });
        }).catch(function () {
            return {
                raceHtml: '',
                levelHtml: ''
            };
        });

        return npcFriendlyDataCache[title];
    }

    function initZoneNpcFriendlyData(root) {
        root = root && root.jquery ? root[0] : (root || document);

        root.querySelectorAll('.zone-monster-table tbody tr, .zone-npc-list-wrapper tbody tr').forEach(function (row) {
            var nameCell = row.querySelector('.zone-monster-name');
            var levelCell = row.querySelector('.zone-monster-level');
            var familyCell = row.querySelector('.zone-monster-family');
            var title;

            if (!nameCell) return;
            if (row.classList.contains('eqoa-npc-friendly-data-loading')) return;

            title = normalizeTitleFromRow(nameCell);
            if (!title) return;

            row.classList.add('eqoa-npc-friendly-data-loading');

            loadNpcFriendlyData(title).then(function (data) {
                if (familyCell && !familyCell.textContent.replace(/\s+/g, ' ').trim() && data.raceHtml) {
                    familyCell.innerHTML = data.raceHtml;
                }

                if (levelCell && !levelCell.textContent.replace(/\s+/g, ' ').trim() && data.levelHtml) {
                    levelCell.innerHTML = data.levelHtml;
                }

                row.classList.add('eqoa-npc-friendly-data-ready');
            });
        });
    }

    function startNpcFriendlyDataObserver() {
        var target;

        if (observerStarted || !window.MutationObserver) return;

        target = document.querySelector('.eqoa-zone-tabs') || document.querySelector('#mw-content-text') || document.body;
        if (!target) return;

        observerStarted = true;

        new MutationObserver(function () {
            initZoneNpcFriendlyData(document);
        }).observe(target, {
            childList: true,
            subtree: true
        });
    }

    mw.hook('wikipage.content').add(function (root) {
        initZoneNpcFriendlyData(root);
        startNpcFriendlyDataObserver();
    });

    document.addEventListener('DOMContentLoaded', function () {
        initZoneNpcFriendlyData(document);
        startNpcFriendlyDataObserver();
    });

    setTimeout(function () {
        initZoneNpcFriendlyData(document);
        startNpcFriendlyDataObserver();
    }, 500);

    setTimeout(function () {
        initZoneNpcFriendlyData(document);
    }, 1500);

    setTimeout(function () {
        initZoneNpcFriendlyData(document);
    }, 3000);
}());






   


function initEqoaAchievements(root) {
    root = root && root.jquery ? root[0] : (root || document);

    var pages = Array.prototype.slice.call(
        root.querySelectorAll('.eqoa-achievements-page')
    );

    if (
        root.classList &&
        root.classList.contains('eqoa-achievements-page')
    ) {
        pages.unshift(root);
    }

    pages.forEach(function (page) {
        var rows = Array.prototype.slice.call(
            page.querySelectorAll('.eqoa-achievement-row')
        );

        var controls = page.querySelector(
            '[data-eqoa-achievement-controls]'
        );

        if (!rows.length || !controls) {
            return;
        }

        var storagePrefix = 'eqoa-achievement-complete:';

        var tabs = [
            {
                label: 'All',
                key: 'All',
                subtabs: []
            },
            {
                label: 'Character',
                key: 'Character',
                subtabs: []
            },
            {
                label: 'World',
                key: 'World',
                subtabs: [
                    'Bestiary',
                    'Arctic',
                    'Desert',
                    'Forest',
                    'Marine',
                    'Mountain',
                    'Plains',
                    'Swamp',
                    'Deep Jungle',
                    'Rathe Mountains',
                    'Red Desert',
                    'Wastelands',
                    'Wetlands'
                ]
            },
            {
                label: 'Quest',
                key: 'Quest',
                subtabs: [
                    'Path Quests',
                    '17',
                    '23',
                    '25',
                    '27',
                    '33',
                    '35',
                    '37',
                    '43',
                    '45',
                    '47',
                    '53',
                    '55',
                    '57',
                    '59',
                    'Misc'
                ]
            },
            {
                label: 'Dungeons and Raids',
                key: 'Dungeons and Raids',
                subtabs: [
                    'Classic',
                    'Permafrost',
                    'Isle of Dread',
                    'Plane of Sky',
                    'Plane of Disease',
                    "Solusek's Eye",
                    'Zaoran Plateau'
                ]
            },
            {
                label: 'Exploration',
                key: 'Exploration',
                subtabs: [
                    'Treasure Chest',
                    'Misc'
                ]
            },
            {
                label: 'Professions and Hobbies',
                key: 'Professions and Hobbies',
                subtabs: [
                    'Armorsmith',
                    'Jewelry',
                    'Tailoring',
                    'Weaponsmith',
                    'Fishing'
                ]
            }
        ];

        function clean(value) {
            return String(value || '')
                .replace(/\s+/g, ' ')
                .trim();
        }

        function norm(value) {
            return clean(value).toLowerCase();
        }

        function get(row, attr) {
            return clean(row.getAttribute(attr));
        }

        function normalizedCategory(row) {
            var category = norm(
                get(row, 'data-achievement-category')
            );

            if (category === 'questing') {
                return 'quest';
            }

            if (
                category === 'dungeon and raids' ||
                category === 'dungeons and raids' ||
                category === 'dungeon' ||
                category === 'raids'
            ) {
                return 'dungeons and raids';
            }

            if (
                category === 'profession' ||
                category === 'hobbies'
            ) {
                return 'professions and hobbies';
            }

            return category;
        }

        function getName(row) {
            return get(row, 'data-achievement-name');
        }

        function storageKey(row) {
            return storagePrefix + norm(getName(row));
        }

        function isComplete(row) {
            try {
                return localStorage.getItem(
                    storageKey(row)
                ) === '1';
            } catch (error) {
                return false;
            }
        }

        function setComplete(row, value) {
            try {
                if (value) {
                    localStorage.setItem(
                        storageKey(row),
                        '1'
                    );
                } else {
                    localStorage.removeItem(
                        storageKey(row)
                    );
                }
            } catch (error) {
                // Ignore unavailable localStorage.
            }
        }

        function parseRequires(value) {
            return String(value || '')
                .split(/\s*\|\|\s*|\s*;\s*|\r?\n/)
                .map(clean)
                .filter(Boolean);
        }

        function findRowByName(name) {
            var wanted = norm(name);

            return rows.find(function (row) {
                return norm(getName(row)) === wanted;
            });
        }

        function clearBrokenIconText(icon) {
            if (!icon) {
                return;
            }

            Array.prototype.slice.call(
                icon.childNodes
            ).forEach(function (node) {
                if (
                    node.nodeType === 3 &&
                    /^[\s?]+$/.test(node.nodeValue || '')
                ) {
                    node.remove();
                }
            });

            icon.querySelectorAll(
                '.mw-broken-media, a.new'
            ).forEach(function (bad) {
                bad.remove();
            });

            if (
                !icon.querySelector('img') &&
                !icon.querySelector(
                    '.eqoa-achievement-icon-fallback'
                )
            ) {
                var fallback = document.createElement('span');

                fallback.className =
                    'eqoa-achievement-icon-fallback';

                fallback.textContent = '?';

                icon.appendChild(fallback);
            }
        }


        function renderRequirements(row) {
            var box = row.querySelector(
                '.eqoa-achievement-requirements'
            );

            if (!box) {
                return;
            }

            var requirements = parseRequires(
                get(row, 'data-achievement-requires')
            );

            box.innerHTML = '';

            if (!requirements.length) {
                row.classList.remove(
                    'is-requirements-open'
                );

                return;
            }

            var title = document.createElement('div');

            title.className =
                'eqoa-achievement-requirements-title';

            title.textContent = 'Required Achievements';

            box.appendChild(title);

            requirements.forEach(function (requirement) {
                var target = findRowByName(requirement);
                var button = document.createElement('button');

                button.type = 'button';
                button.className =
                    'eqoa-achievement-requirement';

                button.textContent = requirement;

                if (target && isComplete(target)) {
                    button.classList.add('is-completed');
                }

                button.addEventListener(
                    'click',
                    function (event) {
                        event.preventDefault();
                        event.stopPropagation();

                        if (!target) {
                            return;
                        }

                        target.scrollIntoView({
                            behavior: 'smooth',
                            block: 'center'
                        });

                        target.classList.add(
                            'is-highlighted'
                        );

                        setTimeout(function () {
                            target.classList.remove(
                                'is-highlighted'
                            );
                        }, 1400);
                    }
                );

                box.appendChild(button);
            });
        }

        function ensureCheckButton(row) {
            var holder = row.querySelector(
                '.eqoa-achievement-check-placeholder'
            );

            if (!holder) {
                return;
            }

            var button = holder.querySelector(
                '.eqoa-achievement-check'
            );

            if (!button) {
                button = document.createElement('button');

                button.type = 'button';
                button.className =
                    'eqoa-achievement-check';

                button.setAttribute(
                    'aria-label',
                    'Toggle achievement'
                );

                button.addEventListener(
                    'click',
                    function (event) {
                        event.preventDefault();
                        event.stopPropagation();

                        setComplete(
                            row,
                            !isComplete(row)
                        );

                        refresh();
                    }
                );

                holder.innerHTML = '';
                holder.appendChild(button);
            }

            var completed = isComplete(row);

            button.classList.toggle(
                'is-checked',
                completed
            );

            button.setAttribute(
                'aria-pressed',
                completed ? 'true' : 'false'
            );

            button.textContent = completed ? '✓' : '';
        }

        function rowMatchesMain(row, main, sub) {
            var category = normalizedCategory(row);

            var subcategory = norm(
                get(row, 'data-achievement-subcategory')
            );

            var biome = norm(
                get(row, 'data-achievement-biome')
            );

            var questtype = norm(
                get(row, 'data-achievement-questtype')
            );

            var npcfamily = norm(
                get(row, 'data-achievement-npcfamily')
            );

            var zone = norm(
                get(row, 'data-achievement-zone')
            );

            var raid = norm(
                get(row, 'data-achievement-raid')
            );

            var dungeon = norm(
                get(row, 'data-achievement-dungeon')
            );

            var wanted = norm(sub);

            if (main === 'All') {
                return true;
            }

            if (main === 'Character') {
                return category === 'character';
            }

            if (main === 'World') {
                if (
                    category !== 'world' &&
                    !biome &&
                    !npcfamily
                ) {
                    return false;
                }

                if (!wanted) {
                    return true;
                }

                if (wanted === 'bestiary') {
                    return (
                        subcategory === 'bestiary' ||
                        category === 'bestiary' ||
                        !!npcfamily
                    );
                }

                return (
                    biome === wanted ||
                    subcategory === wanted
                );
            }

            if (main === 'Quest') {
                if (
                    category !== 'quest' &&
                    !questtype
                ) {
                    return false;
                }

                if (!wanted) {
                    return true;
                }

                return (
                    questtype === wanted ||
                    subcategory === wanted ||
                    norm('Lv' + questtype) === wanted
                );
            }

            if (main === 'Dungeons and Raids') {
                if (
                    category !== 'dungeons and raids' &&
                    !raid &&
                    !dungeon
                ) {
                    return false;
                }

                if (!wanted) {
                    return true;
                }

                return (
                    zone === wanted ||
                    raid === wanted ||
                    dungeon === wanted ||
                    subcategory === wanted
                );
            }

            if (main === 'Exploration') {
                if (category !== 'exploration') {
                    return false;
                }

                if (!wanted) {
                    return true;
                }

                return subcategory === wanted;
            }

            if (main === 'Professions and Hobbies') {
                if (
                    category !==
                    'professions and hobbies'
                ) {
                    return false;
                }

                if (!wanted) {
                    return true;
                }

                return subcategory === wanted;
            }

            return false;
        }

        function buildControls() {
            if (
                controls.classList.contains(
                    'eqoa-achievement-controls-ready'
                ) &&
                controls.querySelector(
                    '.eqoa-achievement-main-tabs'
                )
            ) {
                return;
            }

            controls.classList.add(
                'eqoa-achievement-controls-ready'
            );

            controls.innerHTML = '';

            var mainTabs = document.createElement('div');

            mainTabs.className =
                'eqoa-achievement-main-tabs';

            var subTabs = document.createElement('div');

            subTabs.className =
                'eqoa-achievement-subtabs';

            var filters = document.createElement('div');

            filters.className =
                'eqoa-achievement-filter-row';

            var search = document.createElement('input');

            search.type = 'search';
            search.className =
                'eqoa-achievement-search';

            search.placeholder =
                'Search achievements';

            var showLabel =
                document.createElement('label');

            showLabel.className =
                'eqoa-achievement-toggle-label';

            var show = document.createElement('input');

            show.type = 'checkbox';
            show.className =
                'eqoa-achievement-show-completed';

            show.checked = true;

            showLabel.appendChild(show);

            showLabel.appendChild(
                document.createTextNode(
                    ' Show Completed'
                )
            );

            filters.appendChild(search);
            filters.appendChild(showLabel);

            controls.appendChild(mainTabs);
            controls.appendChild(subTabs);
            controls.appendChild(filters);

            search.addEventListener(
                'input',
                refresh
            );

            show.addEventListener(
                'change',
                refresh
            );

        }

function renderTabs() {
            var mainTabs = controls.querySelector(
                '.eqoa-achievement-main-tabs'
            );

            var subTabs = controls.querySelector(
                '.eqoa-achievement-subtabs'
            );

            if (!mainTabs || !subTabs) {
                return;
            }

            var activeMain =
                controls.getAttribute(
                    'data-active-main'
                ) || 'All';

            var activeSub =
                controls.getAttribute(
                    'data-active-sub'
                ) || '';

            var activeTab =
                tabs.find(function (tab) {
                    return tab.key === activeMain;
                }) || tabs[0];

            mainTabs.innerHTML = '';
            subTabs.innerHTML = '';

            tabs.forEach(function (tab) {
                var button =
                    document.createElement('button');

                button.type = 'button';
                button.textContent = tab.label;

                button.setAttribute(
                    'data-achievement-tab',
                    tab.key
                );

                if (tab.key === activeTab.key) {
                    button.classList.add('is-active');
                }

                button.addEventListener(
                    'click',
                    function () {
                        controls.setAttribute(
                            'data-active-main',
                            tab.key
                        );

                        controls.setAttribute(
                            'data-active-sub',
                            ''
                        );

                        refresh();
                    }
                );

                mainTabs.appendChild(button);
            });

            if (!activeTab.subtabs.length) {
                subTabs.hidden = true;
                return;
            }

            subTabs.hidden = false;

            var allButton =
                document.createElement('button');

            allButton.type = 'button';
            allButton.textContent = 'All';

            allButton.setAttribute(
                'data-achievement-subtab',
                ''
            );

if (!activeSub) {
    allButton.classList.add('is-active');
}

allButton.addEventListener('click', function (event) {
    event.preventDefault();
    event.stopPropagation();

    controls.setAttribute(
        'data-active-sub',
        ''
    );

    refresh();
});

subTabs.appendChild(allButton);

            activeTab.subtabs.forEach(function (sub) {
                var button =
                    document.createElement('button');

                button.type = 'button';
                button.textContent = sub;

                button.setAttribute(
                    'data-achievement-subtab',
                    sub
                );

if (sub === activeSub) {
    button.classList.add('is-active');
}

button.addEventListener('click', function (event) {
    event.preventDefault();
    event.stopPropagation();

    controls.setAttribute(
        'data-active-sub',
        sub
    );

    refresh();
});

subTabs.appendChild(button);
            });
        }

function updateTracker() {
    var counted = rows.filter(function (row) {
        return (
            normalizedCategory(row) !==
            'feats of strength'
        );
    });

    var completed =
        counted.filter(isComplete).length;

    var total = counted.length;

    var percent = total
        ? Math.round(
            completed / total * 100
        )
        : 0;

    var earnedPoints = counted
        .filter(isComplete)
        .reduce(function (sum, row) {
            var value = parseInt(
                get(
                    row,
                    'data-achievement-points'
                ),
                10
            );

            return sum + (
                isNaN(value) ? 0 : value
            );
        }, 0);

    var totalPoints = counted
        .reduce(function (sum, row) {
            var value = parseInt(
                get(
                    row,
                    'data-achievement-points'
                ),
                10
            );

            return sum + (
                isNaN(value) ? 0 : value
            );
        }, 0);

    var percentBox = page.querySelector(
        '.eqoa-achievement-total-percent'
    );

    var countBox = page.querySelector(
        '.eqoa-achievement-total-count'
    );

    var pointsValue = page.querySelector(
        '.eqoa-achievement-points-summary-value'
    );

    var pointsMeta = page.querySelector(
        '.eqoa-achievement-points-summary-meta'
    );

    if (percentBox) {
        percentBox.textContent =
            percent + '%';
    }

    if (countBox) {
        countBox.textContent =
            completed +
            ' / ' +
            total +
            ' Completed';
    }

    if (pointsValue) {
        pointsValue.textContent =
            earnedPoints;
    }

    if (pointsMeta) {
        pointsMeta.textContent =
            earnedPoints +
            ' / ' +
            totalPoints +
            ' earned';
    }

    var groups = {
        Character: function (row) {
            return (
                normalizedCategory(row) ===
                'character'
            );
        },

        World: function (row) {
            return (
                normalizedCategory(row) ===
                    'world' ||
                normalizedCategory(row) ===
                    'bestiary' ||
                !!get(
                    row,
                    'data-achievement-biome'
                ) ||
                !!get(
                    row,
                    'data-achievement-npcfamily'
                )
            );
        },

        Quest: function (row) {
            return (
                normalizedCategory(row) ===
                    'quest' ||
                !!get(
                    row,
                    'data-achievement-questtype'
                )
            );
        },

        'Dungeons and Raids': function (row) {
            return (
                normalizedCategory(row) ===
                    'dungeons and raids' ||
                !!get(
                    row,
                    'data-achievement-raid'
                ) ||
                !!get(
                    row,
                    'data-achievement-dungeon'
                )
            );
        },

        Exploration: function (row) {
            return (
                normalizedCategory(row) ===
                'exploration'
            );
        },

        'Professions and Hobbies':
            function (row) {
                return (
                    normalizedCategory(row) ===
                    'professions and hobbies'
                );
            }
    };

    Object.keys(groups).forEach(
        function (groupName) {
            var box = page.querySelector(
                '[data-achievement-summary="' +
                groupName +
                '"] span'
            );

            if (!box) {
                return;
            }

            var groupRows =
                counted.filter(
                    groups[groupName]
                );

            var groupDone =
                groupRows.filter(
                    isComplete
                ).length;

            box.textContent =
                groupDone +
                ' / ' +
                groupRows.length;
        }
    );
}

        function refresh() {
            buildControls();
            renderTabs();

            var activeMain =
                controls.getAttribute(
                    'data-active-main'
                ) || 'All';

            var activeSub =
                controls.getAttribute(
                    'data-active-sub'
                ) || '';

            var searchInput =
                controls.querySelector(
                    '.eqoa-achievement-search'
                );

            var search = norm(
                searchInput ? searchInput.value : ''
            );

            var showCompleted =
                controls.querySelector(
                    '.eqoa-achievement-show-completed'
                );

            var shouldShowCompleted =
                !showCompleted ||
                showCompleted.checked;

            rows.forEach(function (row) {
                clearBrokenIconText(
                    row.querySelector(
                        '.eqoa-achievement-icon'
                    )
                );

                ensureCheckButton(row);
                renderRequirements(row);

                var complete = isComplete(row);

                row.classList.toggle(
                    'is-completed',
                    complete
                );

                if (
                    !row.classList.contains(
                        'eqoa-achievement-click-ready'
                    )
                ) {
                    row.classList.add(
                        'eqoa-achievement-click-ready'
                    );

                    row.addEventListener(
                        'click',
                        function (event) {
                            if (
                                event.target.closest(
                                    '.eqoa-achievement-check, ' +
                                    '.eqoa-achievement-requirement, ' +
                                    'input, label'
                                )
                            ) {
                                return;
                            }

                            var requirements =
                                parseRequires(
                                    get(
                                        row,
                                        'data-achievement-requires'
                                    )
                                );

                            if (!requirements.length) {
                                return;
                            }

                            event.preventDefault();
                            event.stopPropagation();

                            row.classList.toggle(
                                'is-requirements-open'
                            );
                        }
                    );
                }

var matchesTab = rowMatchesMain(
    row,
    activeMain,
    activeSub
);

                var searchableText = [
                    row.textContent,
                    get(
                        row,
                        'data-achievement-name'
                    ),
                    get(
                        row,
                        'data-achievement-category'
                    ),
                    get(
                        row,
                        'data-achievement-subcategory'
                    ),
                    get(
                        row,
                        'data-achievement-biome'
                    ),
                    get(
                        row,
                        'data-achievement-questtype'
                    ),
                    get(
                        row,
                        'data-achievement-npcfamily'
                    ),
                    get(
                        row,
                        'data-achievement-zone'
                    ),
                    get(
                        row,
                        'data-achievement-raid'
                    ),
                    get(
                        row,
                        'data-achievement-dungeon'
                    ),
                    get(
                        row,
                        'data-achievement-requires'
                    )
                ].join(' ');

                var matchesSearch =
                    !search ||
                    norm(searchableText).indexOf(
                        search
                    ) !== -1;

var matchesCompleted =
    shouldShowCompleted ||
    !complete;

var isVisible =
    matchesTab &&
    matchesSearch &&
    matchesCompleted;

row.classList.toggle(
    'is-filtered-out',
    !isVisible
);

row.hidden = !isVisible;

row.setAttribute(
    'aria-hidden',
    isVisible ? 'false' : 'true'
);

            });

Array.prototype.forEach.call(
    page.querySelectorAll('.eqoa-achievement-list > p'),
    function (paragraph) {
        var text = String(paragraph.textContent || '')
            .replace(/\u00a0/g, ' ')
            .trim();

        var hasMeaningfulContent =
            text.length > 0 ||
            paragraph.querySelector(
                'img, a, input, button, .eqoa-achievement-row'
            );

        paragraph.style.display =
            hasMeaningfulContent ? '' : 'none';
    }
);

            updateTracker();
        }

        refresh();
    });
}

mw.hook('wikipage.content').add(
    initEqoaAchievements
);

document.addEventListener(
    'DOMContentLoaded',
    function () {
        initEqoaAchievements(document);
    }
);

setTimeout(function () {
    initEqoaAchievements(document);
}, 500);

setTimeout(function () {
    initEqoaAchievements(document);
}, 1500);