Shows the number of cities per god and adds single-god filtering to the Gods Overview.
// ==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();
})();