Munzee Specials

Show icons in front of the name of your own creatures at https://www.munzee.com/specials/

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Munzee Specials
// @namespace    https://greatest.deepsurf.us/users/156194
// @version      0.50
// @description  Show icons in front of the name of your own creatures at https://www.munzee.com/specials/
// @author       rabe85
// @match        https://www.munzee.com/specials
// @match        https://www.munzee.com/specials/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=munzee.com
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_listValues
// @grant        GM_deleteValue
// ==/UserScript==

(function() {
    'use strict';


    // ============================================================================
    // A) Warten auf ein Element (Promise)
    // ============================================================================
    function waitForElm(selector) {
        return new Promise(resolve => {
            const found = document.querySelector(selector);
            if (found) return resolve(found);

            const observer = new MutationObserver(() => {
                const elem = document.querySelector(selector);
                if (elem) {
                    resolve(elem);
                    observer.disconnect();
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    }


    // ============================================================================
    // B) Zählen der eigenen Specials
    // ============================================================================
    function countOwnCreatures() {
        const container = document.querySelector(".alert.alert-info");
        if (!container) return 0;
        return container.children.length;
    }


    // ============================================================================
    // C) Counter aktualisieren
    // ============================================================================
    function updateOwnCreaturesCounter() {
        const count = countOwnCreatures();
        const counterSpan = document.getElementById("own_creatures_counter");
        if (counterSpan) {
            counterSpan.textContent = count + " Own Creatures";
        }
    }


    // ============================================================================
    // D) Name aus erstem <a> extrahieren + Dateiname ableiten
    // ============================================================================

    // Beispiele:
    // "Your Yeti Munzee #66"        -> "Yeti"
    // "Your Pimedus #3"             -> "Pimedus"
    // "Your Ghost of Christmas Future Munzee #12" -> "Ghost of Christmas Future"
    function extractCreatureName(text) {
        let cleaned = text.replace(/^Your\s+/i, ""); // "Yeti Munzee #66"
        cleaned = cleaned.replace(/\s+#\d+.*$/i, ""); // "Yeti Munzee"
        cleaned = cleaned.replace(/\s+Munzee$/i, ""); // "Yeti"
        return cleaned.trim();
    }

    function autoFileName(name) {
        return name.toLowerCase().replace(/\s+/g, "") + ".png";
    }

    // Manuelle Overrides für die Fälle, wo der Dateiname nicht automatisch passt
    const creatureIconsOverride = {
        "Unicorn": "theunicorn.png",
    };


    // ============================================================================
    // E) Bild-Existenz prüfen (CORS-sicher über <img>)
    // ============================================================================
    function fileExists(url) {
        return new Promise(resolve => {
            const img = new Image();
            img.onload = () => resolve(true);
            img.onerror = () => resolve(false);
            img.src = url + "?cb=" + Date.now();
        });
    }


    // ============================================================================
    // F) Dynamische Verarbeitung der eigenen Specials (stabil, kein innerHTML)
    // ============================================================================
    const processedOwnSpecials = new WeakSet();

    async function processOwnSpecial(row, url) {
        if (processedOwnSpecials.has(row)) return;
        processedOwnSpecials.add(row);

        const firstLink = row.querySelector("a");
        if (!firstLink) return;

        const text = firstLink.textContent;
        const creatureName = extractCreatureName(text);

        let file = creatureIconsOverride[creatureName] || autoFileName(creatureName);
        const fullUrl = url + file;

        const exists = await fileExists(fullUrl);
        if (!exists) {
            console.warn("Kein Icon gefunden für:", creatureName, "→", file);
            return; // KEIN ICON EINBLENDEN
        }

        const img = document.createElement("img");
        img.src = fullUrl;
        img.alt = creatureName;
        img.title = creatureName;
        img.style.maxHeight = "32px";
        img.style.marginRight = "4px";

        row.insertBefore(img, firstLink);
    }


    // ============================================================================
    // G) Observer für eigene Specials (mit Debounce + parallelem Check)
    // ============================================================================
    let ownSpecialsTimer = null;

    function observeOwnSpecials(url) {
        const container = document.querySelector(".alert.alert-info");
        if (!container) return;

        async function processAll() {
            const rows = Array.from(container.children);
            const tasks = rows.map(row => processOwnSpecial(row, url));
            await Promise.all(tasks);
            updateOwnCreaturesCounter();
        }

        processAll();

        const observer = new MutationObserver(() => {
            clearTimeout(ownSpecialsTimer);
            ownSpecialsTimer = setTimeout(processAll, 50);
        });

        observer.observe(container, {
            childList: true,
            subtree: false
        });
    }


    // ============================================================================
    // H) LEGENDEN – Verarbeitung EINER grid-row (mit Debounce)
    // ============================================================================
    const processedLegendRows = new WeakSet();
    let legendTimer = null;

    function munzee_specials_legend(row) {
        const img = row.querySelector("img");
        if (!img) return;

        const parts = img.src.split("/");
        const filename = parts[parts.length - 1].split(".")[0];
        const label = filename.charAt(0).toUpperCase() + filename.slice(1);

        img.setAttribute("title", label);
        img.setAttribute("alt", label);
    }

    function handleLegendRow(row) {
        if (!processedLegendRows.has(row)) {
            processedLegendRows.add(row);
            munzee_specials_legend(row);
        }
    }

    function observeLegendRows() {
        const container = document.querySelector(".captures-grid-rows");
        if (!container) return;

        function processAll() {
            document.querySelectorAll(".captures-grid-rows .grid-row")
                .forEach(handleLegendRow);
        }

        processAll();

        const observer = new MutationObserver(() => {
            clearTimeout(legendTimer);
            legendTimer = setTimeout(processAll, 50);
        });

        observer.observe(container, {
            childList: true,
            subtree: true
        });
    }


    // ============================================================================
    // I) BUTTON-VERARBEITUNG – EINMAL PRO BUTTON (mit Debounce)
    // ============================================================================
    const processedButtons = new WeakSet();
    let buttonTimer = null;

    function munzee_specials_button(btn) {
        const img = btn.querySelector("img");
        if (!img) return;

        const parts = img.src.split("/");
        const filename = parts[parts.length - 1].split(".")[0];
        const label = filename.charAt(0).toUpperCase() + filename.slice(1);

        img.setAttribute("title", label);
        img.setAttribute("alt", label);
    }

    function handleButton(btn) {
        if (!processedButtons.has(btn)) {
            processedButtons.add(btn);
            munzee_specials_button(btn);
        }
    }

    function observeButtons(selector) {
        function processAll() {
            document.querySelectorAll(selector).forEach(handleButton);
        }

        processAll();

        const observer = new MutationObserver(() => {
            clearTimeout(buttonTimer);
            buttonTimer = setTimeout(processAll, 50);
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }


    // ============================================================================
    // J) INITIALISIERUNG – vollständig integriert
    // ============================================================================
    async function munzee_specials_init() {

        await waitForElm(".captures-grid-rows");

        var munzee_setting_specials_url = GM_getValue('munzee_setting_specials_url', 'v4');

        function save_settings_v3pins() {
            GM_setValue('munzee_setting_specials_url', 'v3');
            location.reload();
        }

        function save_settings_v4pins() {
            GM_setValue('munzee_setting_specials_url', 'v4');
            location.reload();
        }

        var specials_url_link = "";
        var own_specials_map = document.getElementsByClassName('map-wrap')[0]?.parentNode;
        var own_creatures_count = countOwnCreatures();

        if (munzee_setting_specials_url == 'v3') {
            specials_url_link =
                `<div style="font-family: Ubuntu,sans-serif; font-weight: 400; font-style: italic; font-size: 23px; color: #999; border-bottom: 1px solid #999; margin-bottom: 10px; padding-bottom: 30px;">
                <span id="own_creatures_counter" style="float:left;">${own_creatures_count} Own Creatures</span>
                <span style="float:right; cursor: pointer; font-size: small; margin-top: 10px;" id="save_settings_v4pins">Show v4 Pins</span>
            </div>`;
            if (own_specials_map) {
                own_specials_map.insertAdjacentHTML('beforebegin', specials_url_link);
                document.getElementById('save_settings_v4pins').addEventListener("click", save_settings_v4pins, false);
            }
        } else {
            specials_url_link =
                `<div style="font-family: Ubuntu,sans-serif; font-weight: 400; font-style: italic; font-size: 23px; color: #999; border-bottom: 1px solid #999; margin-bottom: 10px; padding-bottom: 30px;">
                <span id="own_creatures_counter" style="float:left;">${own_creatures_count} Own Creatures</span>
                <span style="float:right; cursor: pointer; font-size: small; margin-top: 10px;" id="save_settings_v3pins">Show v3 Pins</span>
            </div>`;
            if (own_specials_map) {
                own_specials_map.insertAdjacentHTML('beforebegin', specials_url_link);
                document.getElementById('save_settings_v3pins').addEventListener("click", save_settings_v3pins, false);
            }
        }

        var url = munzee_setting_specials_url === "v3"
        ? "https://www.otb-server.de/munzee/v3pins/"
        : "https://munzee.global.ssl.fastly.net/images/pins/";

        observeOwnSpecials(url);
        observeLegendRows();
    }


    // ============================================================================
    // K) STARTLOGIK
    // ============================================================================
    function start_munzee_specials() {
        munzee_specials_init();
        observeButtons("button.icon-btn");
    }

    if (document.readyState === "complete" || document.readyState === "interactive") {
        start_munzee_specials();
    } else {
        document.addEventListener("DOMContentLoaded", start_munzee_specials);
    }


})();