Grepolis God Counter

Shows the number of cities per god and adds single-god filtering to the Gods Overview.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         Grepolis God Counter
// @namespace    https://greasyfork.org/
// @version      1.0.0
// @author       The Invincble
// @description  Shows the number of cities per god and adds single-god filtering to the Gods Overview.
// @match        https://*.grepolis.com/game/*
// @match        https://*.grepolis.nl/game/*
// @match        https://*.grepolis.de/game/*
// @match        https://*.grepolis.net/game/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function () {
    "use strict";

    const PANEL_CLASS = "god-city-counter";
    const STYLE_ID = "god-city-counter-styles";

    const GOD_NAMES = {
        zeus: "Zeus",
        poseidon: "Poseidon",
        hera: "Hera",
        athena: "Athena",
        hades: "Hades",
        artemis: "Artemis",
        aphrodite: "Aphrodite",
        ares: "Ares",
        none: "No god"
    };

    const GOD_ORDER = [
        "zeus",
        "poseidon",
        "hera",
        "athena",
        "hades",
        "artemis",
        "aphrodite",
        "ares",
        "none"
    ];

    const initializedWrappers = new WeakSet();
    const initializationTimers = new WeakMap();

    let scanTimer = null;

    function addStyles() {
        if (document.getElementById(STYLE_ID)) {
            return;
        }

        const style = document.createElement("style");
        style.id = STYLE_ID;

        style.textContent = `
            /*
             * Main panel.
             * Uses the existing Grepolis window background.
             */
            .${PANEL_CLASS} {
                clear: both;
                margin: 8px 18px 12px;
                padding: 0 0 9px;
                color: #4b2f12;
                font-family: Arial, Helvetica, sans-serif;
                font-size: 11px;
                background: transparent;
                border-bottom: 1px solid rgba(90, 59, 18, 0.38);
            }

            .uti-save-btn {
                height: 20px;
                min-width: 78px;
                padding: 0 8px;
                border: 1px solid #8a672c;
                border-radius: 2px;
                background: #0F213A;
                color: wheat;
                font-size: 10px;
                font-weight: bold;
                cursor: pointer;
                box-sizing: border-box;
                text-align: center;
                box-shadow: inset 0 1px 0 rgba(255,255,255,0.6);
                align-content: center;
            }

            .uti-save-btn:hover {
                background: linear-gradient(#fff9dc, #e6c985);
                color: #0F213A;
            }

            /*
             * Section title based on the Unit Production script.
             */
            .${PANEL_CLASS} .counter-title {
                margin: 0 0 9px;
                padding: 0 2px 5px;
                color: #5a3b12;
                font-family: Georgia, "Times New Roman", serif;
                font-size: 16px;
                font-weight: bold;
                line-height: 20px;
                text-align: left;
                text-shadow: 0 1px rgba(255, 244, 198, 0.8);
                border-bottom: 1px solid rgba(90, 59, 18, 0.35);
            }

            /*
             * God grid.
             */
            .${PANEL_CLASS} .counter-list {
                display: grid;
                grid-template-columns:
                    repeat(auto-fit, minmax(122px, 1fr));
                gap: 5px 12px;
                padding: 0 2px;
            }

            /*
             * Individual god.
             * No large card, shadow or modern rounded border.
             */
            .${PANEL_CLASS} .counter-item {
                display: flex;
                align-items: center;
                min-width: 0;
                height: 32px;
                padding: 2px 5px 2px 2px;
                box-sizing: border-box;
                color: #3b240e;
                cursor: pointer;
                user-select: none;
                border: 1px solid transparent;
                background: transparent;
            }

            .${PANEL_CLASS} .counter-item:hover {
                border-color: rgba(102, 68, 29, 0.45);
                background: rgba(255, 240, 184, 0.3);
            }

            /*
             * Selected god.
             */
            .${PANEL_CLASS} .counter-item.active {
                border-color: #657f35;
                background: rgba(181, 205, 116, 0.48);
                box-shadow:
                    inset 0 0 3px rgba(63, 91, 31, 0.3);
            }

            .${PANEL_CLASS} .counter-item.inactive {
                opacity: 0.48;
            }

            /*
             * God icon.
             */
            .${PANEL_CLASS} .counter-icon {
                display: flex;
                align-items: center;
                justify-content: center;
                width: 32px;
                min-width: 32px;
                height: 30px;
                margin-right: 5px;
                overflow: hidden;
            }

            .${PANEL_CLASS} .counter-icon .god_micro {
                margin: 0;
                transform: scale(1);
                transform-origin: center;
            }

            .${PANEL_CLASS} .counter-no-god {
                color: #a31c1c;
                font-family: Georgia, serif;
                font-size: 24px;
                font-weight: normal;
                line-height: 28px;
                text-shadow:
                    1px 1px rgba(255, 240, 190, 0.7);
            }

            /*
             * God name.
             */
            .${PANEL_CLASS} .counter-name {
                flex: 1;
                min-width: 0;
                overflow: hidden;
                color: #3c250f;
                font-weight: bold;
                text-overflow: ellipsis;
                white-space: nowrap;
            }

            /*
             * City count.
             */
            .${PANEL_CLASS} .counter-value {
                display: inline-block;
                min-width: 25px;
                margin-left: 5px;
                padding: 1px 4px;
                box-sizing: border-box;
                color: #fff2c6;
                font-size: 11px;
                font-weight: bold;
                line-height: 16px;
                text-align: center;
                text-shadow: 1px 1px #2a1606;
                border: 1px solid #60401e;
                background:
                    linear-gradient(
                        to bottom,
                        #805b31,
                        #533416
                    );
                box-shadow:
                    inset 0 1px rgba(255, 255, 255, 0.22);
            }

            /*
             * Footer.
             */
            .${PANEL_CLASS} .counter-footer {
                display: flex;
                flex-wrap: wrap;
                align-items: center;
                justify-content: center;
                gap: 8px 16px;
                min-height: 28px;
                margin-top: 9px;
                padding: 7px 2px 0;
                color: #3d260f;
                border-top: 1px solid rgba(90, 59, 18, 0.28);
            }

            .${PANEL_CLASS} .counter-stat {
                font-weight: bold;
                white-space: nowrap;
            }

            .${PANEL_CLASS} .counter-stat-label {
                font-weight: normal;
            }

            /*
             * Native-style Grepolis button markup.
             */
            .${PANEL_CLASS} .counter-show-all {
                display: inline-block;
                position: relative;
                cursor: pointer;
                text-decoration: none;
            }

            .${PANEL_CLASS} .uti-save-btn.disabled {
                opacity: 0.55;
                cursor: default;
                pointer-events: none;
            }

            /*
             * Fallback button styling in case the world's
             * button_new styles differ or are unavailable.
             */
            .${PANEL_CLASS} .counter-show-all.button_new {
                min-width: 72px;
                height: 23px;
            }

            .${PANEL_CLASS}
            .counter-show-all.button_new
            .middle {
                min-width: 54px;
                text-align: center;
            }

            /*
             * Ensure counter remains above the town list.
             */
            #gods_overview_wrapper .${PANEL_CLASS}
            + #gods_overview_towns {
                clear: both;
            }
        `;

        document.head.appendChild(style);
    }

    function getTownList(wrapper) {
        return wrapper.querySelector(
            "#gods_overview_towns"
        );
    }

    function getTowns(wrapper) {
        const townList = getTownList(wrapper);

        if (!townList) {
            return [];
        }

        return Array.from(
            townList.querySelectorAll(
                ".town_item[data-god_id]"
            )
        );
    }

    function getGodIcons(wrapper) {
        return Array.from(
            wrapper.querySelectorAll(
                ".god_icons_container " +
                ".gods_overview_icon[data-god_id]"
            )
        );
    }

    function getTownGod(town) {
        return (town.dataset.god_id || "")
            .trim()
            .toLowerCase();
    }

    function countGods(wrapper) {
        const counts = {};

        GOD_ORDER.forEach(godId => {
            counts[godId] = 0;
        });

        const towns = getTowns(wrapper);

        towns.forEach(town => {
            const godId =
                getTownGod(town) || "none";

            if (!(godId in counts)) {
                counts[godId] = 0;
            }

            counts[godId]++;
        });

        return {
            counts,
            total: towns.length
        };
    }

    function getActiveFilter(wrapper) {
        return wrapper.dataset.godFilter || "all";
    }

    function setActiveFilter(wrapper, godId) {
        if (wrapper.dataset.godFilter !== godId) {
            wrapper.dataset.godFilter = godId;
        }
    }

    function setClassState(
        element,
        className,
        enabled
    ) {
        if (
            element.classList.contains(className) !==
            enabled
        ) {
            element.classList.toggle(
                className,
                enabled
            );
        }
    }

    function updateGodIcons(wrapper, activeGod) {
        getGodIcons(wrapper).forEach(icon => {
            const godId = icon.dataset.god_id;

            let selected = true;

            if (activeGod === "none") {
                selected = false;
            } else if (activeGod !== "all") {
                selected = godId === activeGod;
            }

            setClassState(
                icon,
                "selected",
                selected
            );
        });
    }

    function applyTownFilter(wrapper, activeGod) {
        getTowns(wrapper).forEach(town => {
            const townGod = getTownGod(town);

            let visible = true;

            if (activeGod === "none") {
                visible = townGod === "";
            } else if (activeGod !== "all") {
                visible = townGod === activeGod;
            }

            const requiredDisplay =
                visible ? "" : "none";

            if (
                town.style.display !==
                requiredDisplay
            ) {
                town.style.display =
                    requiredDisplay;
            }
        });
    }

    function updateCounterSelection(wrapper) {
        const panel = wrapper.querySelector(
            `.${PANEL_CLASS}`
        );

        if (!panel) {
            return;
        }

        const activeGod = getActiveFilter(wrapper);

        panel
            .querySelectorAll(".counter-item")
            .forEach(item => {
                const godId = item.dataset.godId;

                const active =
                    activeGod !== "all" &&
                    godId === activeGod;

                const inactive =
                    activeGod !== "all" &&
                    !active;

                setClassState(
                    item,
                    "active",
                    active
                );

                setClassState(
                    item,
                    "inactive",
                    inactive
                );
            });

        const showAllButton = panel.querySelector(
            ".uti-save-btn"
        );

        if (showAllButton) {
            setClassState(
                showAllButton,
                "disabled",
                activeGod === "all"
            );
        }
    }

    function updateVisibleCounter(wrapper) {
        const panel = wrapper.querySelector(
            `.${PANEL_CLASS}`
        );

        if (!panel) {
            return;
        }

        const visibleCount = getTowns(wrapper)
            .filter(town => {
                return town.style.display !== "none";
            })
            .length;

        const valueElement = panel.querySelector(
            ".counter-visible-value"
        );

        if (!valueElement) {
            return;
        }

        const requiredText =
            String(visibleCount);

        if (
            valueElement.textContent !==
            requiredText
        ) {
            valueElement.textContent =
                requiredText;
        }
    }

    function applyFilter(wrapper, godId) {
        if (getActiveFilter(wrapper) === godId) {
            return;
        }

        setActiveFilter(wrapper, godId);
        updateGodIcons(wrapper, godId);
        applyTownFilter(wrapper, godId);
        updateCounterSelection(wrapper);
        updateVisibleCounter(wrapper);
    }

    function toggleFilter(wrapper, godId) {
        const activeGod =
            getActiveFilter(wrapper);

        applyFilter(
            wrapper,
            activeGod === godId
                ? "all"
                : godId
        );
    }

    function createGodIcon(godId) {
        const iconWrapper =
            document.createElement("div");

        iconWrapper.className =
            "counter-icon";

        if (godId === "none") {
            const noGodIcon =
                document.createElement("span");

            noGodIcon.className =
                "counter-no-god";

            noGodIcon.textContent = "×";

            iconWrapper.appendChild(
                noGodIcon
            );

            return iconWrapper;
        }

        const godIcon =
            document.createElement("div");

        godIcon.className =
            `god_micro town_god ${godId}`;

        iconWrapper.appendChild(godIcon);

        return iconWrapper;
    }

    function createCounterItem(
        wrapper,
        godId,
        amount
    ) {
        const item =
            document.createElement("div");

        item.className = "counter-item";
        item.dataset.godId = godId;

        if (godId === "none") {
            item.title =
                "Show cities without a god";
        } else {
            item.title =
                `Show cities worshipping ` +
                `${GOD_NAMES[godId] || godId}`;
        }

        const icon = createGodIcon(godId);

        const name =
            document.createElement("span");

        name.className = "counter-name";
        name.textContent =
            GOD_NAMES[godId] || godId;

        const value =
            document.createElement("span");

        value.className = "counter-value";
        value.textContent = String(amount);

        item.append(icon, name, value);

        item.addEventListener(
            "click",
            event => {
                event.preventDefault();
                event.stopPropagation();

                toggleFilter(
                    wrapper,
                    godId
                );
            }
        );

        return item;
    }

    function createStat(
        label,
        value,
        valueClass
    ) {
        const stat =
            document.createElement("span");

        stat.className = "counter-stat";

        const labelElement =
            document.createElement("span");

        labelElement.className =
            "counter-stat-label";

        labelElement.textContent =
            `${label}: `;

        const valueElement =
            document.createElement("strong");

        valueElement.className = valueClass;
        valueElement.textContent =
            String(value);

        stat.append(
            labelElement,
            valueElement
        );

        return stat;
    }

    function createShowAllButton(wrapper) {
        const button =
            document.createElement("a");

        button.href = "#";
        button.className =
            "button_new uti-save-btn disabled";

        button.innerHTML = `
                    <span class="middle">
                        Show all
                    </span>
        `;

        button.addEventListener(
            "click",
            event => {
                event.preventDefault();
                event.stopPropagation();

                if (
                    getActiveFilter(wrapper) !==
                    "all"
                ) {
                    applyFilter(
                        wrapper,
                        "all"
                    );
                }
            }
        );

        return button;
    }

    function createCounterPanel(wrapper) {
        const townList = getTownList(wrapper);

        if (!townList) {
            return null;
        }

        const existingPanel =
            wrapper.querySelector(
                `.${PANEL_CLASS}`
            );

        if (existingPanel) {
            return existingPanel;
        }

        const { counts, total } =
            countGods(wrapper);

        const panel =
            document.createElement("div");

        panel.className = PANEL_CLASS;

        const title =
            document.createElement("div");

        title.className = "counter-title";
        title.textContent = "God Counter";

        const list =
            document.createElement("div");

        list.className = "counter-list";

        GOD_ORDER.forEach(godId => {
            list.appendChild(
                createCounterItem(
                    wrapper,
                    godId,
                    counts[godId] || 0
                )
            );
        });

        Object.keys(counts)
            .filter(godId => {
                return !GOD_ORDER.includes(
                    godId
                );
            })
            .sort()
            .forEach(godId => {
                list.appendChild(
                    createCounterItem(
                        wrapper,
                        godId,
                        counts[godId]
                    )
                );
            });

        const footer =
            document.createElement("div");

        footer.className = "counter-footer";

        const totalStat = createStat(
            "Total cities",
            total,
            "counter-total-value"
        );

        const visibleStat = createStat(
            "Visible cities",
            total,
            "counter-visible-value"
        );

        const showAllButton =
            createShowAllButton(wrapper);

        footer.append(
            totalStat,
            visibleStat,
            showAllButton
        );

        panel.append(
            title,
            list,
            footer
        );

        townList.insertAdjacentElement(
            "beforebegin",
            panel
        );

        return panel;
    }

    function enableSingleGodSelection(wrapper) {
        wrapper.addEventListener(
            "click",
            event => {
                const icon = event.target.closest(
                    ".god_icons_container " +
                    ".gods_overview_icon[data-god_id]"
                );

                if (
                    !icon ||
                    !wrapper.contains(icon)
                ) {
                    return;
                }

                const godId =
                    icon.dataset.god_id;

                if (!godId) {
                    return;
                }

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

                toggleFilter(
                    wrapper,
                    godId
                );
            },
            true
        );

        wrapper.addEventListener(
            "click",
            event => {
                const button = event.target.closest(
                    ".god_icons_container " +
                    ".filter_buttons a"
                );

                if (
                    !button ||
                    !wrapper.contains(button)
                ) {
                    return;
                }

                const isFilterButton =
                    button.classList.contains(
                        "cancel"
                    ) ||
                    button.classList.contains(
                        "confirm"
                    );

                if (!isFilterButton) {
                    return;
                }

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

                if (
                    getActiveFilter(wrapper) !==
                    "all"
                ) {
                    applyFilter(
                        wrapper,
                        "all"
                    );
                }
            },
            true
        );
    }

    function initializeOverview(wrapper) {
        if (
            !wrapper ||
            initializedWrappers.has(wrapper)
        ) {
            return;
        }

        const townList =
            getTownList(wrapper);

        const towns =
            getTowns(wrapper);

        /*
         * Wait until Grepolis has loaded
         * the complete city list.
         */
        if (
            !townList ||
            towns.length === 0
        ) {
            return;
        }

        /*
         * Mark as initialized before adding
         * the panel, preventing duplicate loading.
         */
        initializedWrappers.add(wrapper);

        setActiveFilter(wrapper, "all");
        enableSingleGodSelection(wrapper);
        createCounterPanel(wrapper);

        updateGodIcons(wrapper, "all");
        applyTownFilter(wrapper, "all");
        updateCounterSelection(wrapper);
        updateVisibleCounter(wrapper);
    }

    function scheduleInitialization(wrapper) {
        if (
            !wrapper ||
            initializedWrappers.has(wrapper)
        ) {
            return;
        }

        const existingTimer =
            initializationTimers.get(wrapper);

        if (existingTimer) {
            window.clearTimeout(
                existingTimer
            );
        }

        /*
         * Wait until Grepolis stops adding
         * cities for 350 ms.
         */
        const timer =
            window.setTimeout(() => {
                initializationTimers.delete(
                    wrapper
                );

                initializeOverview(
                    wrapper
                );
            }, 350);

        initializationTimers.set(
            wrapper,
            timer
        );
    }

    function scanForOverviews() {
        document
            .querySelectorAll(
                "#gods_overview_wrapper"
            )
            .forEach(wrapper => {
                scheduleInitialization(
                    wrapper
                );
            });
    }

    function scheduleScan() {
        window.clearTimeout(scanTimer);

        scanTimer =
            window.setTimeout(() => {
                scanForOverviews();
            }, 75);
    }

    function nodeContainsOverview(node) {
        if (!(node instanceof HTMLElement)) {
            return false;
        }

        /*
         * Ignore everything created inside
         * the custom panel.
         */
        if (
            node.classList.contains(
                PANEL_CLASS
            ) ||
            node.closest?.(
                `.${PANEL_CLASS}`
            )
        ) {
            return false;
        }

        return (
            node.id ===
                "gods_overview_wrapper" ||
            node.id ===
                "gods_overview_towns" ||
            node.matches?.(".town_item") ||
            Boolean(
                node.querySelector?.(
                    "#gods_overview_wrapper, " +
                    "#gods_overview_towns, " +
                    ".town_item"
                )
            )
        );
    }

    function startObserver() {
        const observer =
            new MutationObserver(
                mutations => {
                    const shouldScan =
                        mutations.some(
                            mutation => {
                                return Array
                                    .from(
                                        mutation
                                            .addedNodes
                                    )
                                    .some(
                                        node =>
                                            nodeContainsOverview(
                                                node
                                            )
                                    );
                            }
                        );

                    if (shouldScan) {
                        scheduleScan();
                    }
                }
            );

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

    addStyles();
    scanForOverviews();
    startObserver();
})();