💪 Anabole Eroberung

Panel mit Welteroberungen nach Spieler, Stamm, Dorf und Uhrzeit.

Αυτός ο κώδικας δεν πρέπει να εγκατασταθεί άμεσα. Είναι μια βιβλιοθήκη για άλλους κώδικες που περιλαμβάνεται μέσω της οδηγίας meta // @require https://update.greasyfork.org/scripts/587739/1879995/%F0%9F%92%AA%20Anabole%20Eroberung.js

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το 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         💪 Anabole Eroberung
// @namespace    https://anaboles.local/
// @version      1.0.41
// @description  Panel mit Welteroberungen nach Spieler, Stamm, Dorf und Uhrzeit.
// @match        https://*.die-staemme.de/game.php*
// @include      *://*.die-staemme.de/game.php*
// @grant        none
// @run-at       document-idle
// @icon         https://dsen.innogamescdn.com/asset/dbeaf8db/favicon.ico
// ==/UserScript==

(() => {
    "use strict";

    if (window.top !== window.self) return;
    if (!/\.die-staemme\.de$/i.test(window.location.hostname)) return;

    // Hub-Check: respektiert den Ein/Aus-Schalter im Anaboles Hub (id: 'eroberung')
    function isEnabledInHub() {
        try {
            const reg = JSON.parse(localStorage.getItem('intellifarm_registry') || '{}');
            return reg.eroberung !== false; // Default: aktiviert, falls Hub fehlt/Eintrag fehlt
        } catch { return true; }
    }
    if (!isEnabledInHub()) return;

    const APP = {
        id: "anabconq",
        version: "1.0.41",
        dialogId: "anabconqWorldConquests",
        title: "Anabole Eroberung",
        mapCacheMs: 55 * 60 * 1000,
        conquerCacheMs: 90 * 1000,
        maxStoredChars: 1800000,
        defaultLimit: 250,
        mapMarkerMax: 250,
        mapMarkerSearchMax: 5000,
        autoRefreshMinMs: 2 * 60 * 1000,
        autoRefreshMaxMs: 5 * 60 * 1000,
    };

    const state = {
        panel: null,
        launcher: null,
        busy: false,
        mapsLoadedAt: 0,
        conquestsLoadedAt: 0,
        lastConquerPath: "",
        rows: [],
        maps: {
            villages: new Map(),
            players: new Map(),
            tribes: new Map(),
        },
        controls: {},
        sortKey: "recent",
        autoTimer: null,
        mapMarkerTimer: null,
        mapLoadButton: null,
        mapButtonForceMarkers: false,
        panelSettingsDraft: null,
        launcherPositionFrame: 0,
        memoryCache: new Map(),
    };

    function init() {
        injectStyle();
        createLauncher();
        scheduleLauncherPosition();
        ensureMapLoadButton();
        window.addEventListener("resize", scheduleLauncherPosition);
        window.addEventListener("orientationchange", scheduleLauncherPosition);
        document.addEventListener("keydown", (event) => {
            if (event.key === "Escape" && state.panel && !state.panel.classList.contains(`${APP.id}-hidden`)) {
                closePanel();
            }
        });
        window.setInterval(() => {
            scheduleLauncherPosition();
            ensureMapLoadButton();
            if (state.rows.length && markMapEnabled()) scheduleMapMarkers(0);
        }, 3000);
    }

    function injectStyle() {
        if (document.getElementById(`${APP.id}-style`)) return;
        const style = document.createElement("style");
        style.id = `${APP.id}-style`;
        style.textContent = `
            #${APP.id}-launcher {
                position: fixed !important;
                left: 12px;
                right: auto !important;
                top: 370px;
                bottom: auto !important;
                z-index: 2147483647;
                box-sizing: border-box;
                display: flex;
                align-items: center;
                justify-content: flex-start;
                gap: 0;
                width: 30px;
                min-width: 30px;
                height: 28px;
                overflow: hidden;
                border: 1px solid #30363d;
                border-radius: 2px;
                background: linear-gradient(to bottom, #c9a84c, #8a6f2e 55%, #0d1117);
                box-shadow: inset 0 1px 0 rgba(255,255,255,.35), inset 0 -1px 0 rgba(0,0,0,.35), 0 2px 5px rgba(0,0,0,.45);
                color: #fff;
                font: bold 12px Verdana, Arial, sans-serif;
                text-shadow: 1px 1px 1px #000;
                white-space: nowrap;
                padding: 0 6px;
                cursor: pointer;
                transition: width .18s ease, min-width .18s ease, padding .18s ease, gap .18s ease, background .18s ease;
            }
            #${APP.id}-launcher:hover,
            #${APP.id}-launcher:focus-visible {
                width: 244px;
                min-width: 244px;
                gap: 8px;
                padding: 0 9px;
                background: linear-gradient(to bottom, #e8c873, #c9a84c 55%, #8a6f2e);
            }
            #${APP.id}-launcher .${APP.id}-launcher-icon {
                position: relative;
                display: flex;
                align-items: center;
                justify-content: center;
                width: 16px;
                height: 15px;
                flex: 0 0 16px;
                box-sizing: border-box;
                font-size: 13px;
                line-height: 1;
                filter: drop-shadow(0 1px 1px rgba(0,0,0,.5));
            }
            #${APP.id}-launcher .${APP.id}-launcher-text {
                display: inline-block;
                max-width: 0;
                opacity: 0;
                overflow: hidden;
                white-space: nowrap;
                transform: translateX(-4px);
                transition: max-width .18s ease, opacity .14s ease, transform .18s ease;
            }
            #${APP.id}-launcher:hover .${APP.id}-launcher-text,
            #${APP.id}-launcher:focus-visible .${APP.id}-launcher-text {
                max-width: 198px;
                opacity: 1;
                transform: translateX(0);
            }
            #${APP.id}-panel,
            #${APP.id}-panel * {
                box-sizing: border-box;
            }
            #popup_box_${APP.dialogId} {
                position: fixed !important;
                top: 50% !important;
                left: 50% !important;
                right: auto !important;
                bottom: auto !important;
                transform: translate(-50%, -50%) !important;
                margin: 0 !important;
                width: min(1320px, calc(100vw - 24px)) !important;
                max-width: calc(100vw - 24px) !important;
                max-height: calc(100vh - 8px) !important;
                box-sizing: border-box !important;
                z-index: 20002 !important;
                overflow: visible !important;
            }
            .popup_box:has(.${APP.id}-shell),
            [id^="popup_box_"]:has(.${APP.id}-shell) {
                position: fixed !important;
                top: 50% !important;
                left: 50% !important;
                right: auto !important;
                bottom: auto !important;
                transform: translate(-50%, -50%) !important;
                margin: 0 !important;
                width: min(1320px, calc(100vw - 24px)) !important;
                max-width: calc(100vw - 24px) !important;
                max-height: calc(100vh - 8px) !important;
                box-sizing: border-box !important;
                overflow: visible !important;
            }
            #popup_box_${APP.dialogId} .popup_box_content,
            #popup_box_${APP.dialogId} .popup_box_content > div {
                width: 100% !important;
                max-width: 100% !important;
                min-width: 0 !important;
                overflow-x: hidden !important;
                overflow-y: hidden !important;
            }
            #popup_box_${APP.dialogId} .${APP.id}-frame {
                width: 100% !important;
                max-width: 100% !important;
            }
            #popup_box_${APP.dialogId} .${APP.id}-shell {
                width: 100% !important;
                max-width: 100% !important;
            }
            #${APP.id}-panel {
                position: fixed;
                z-index: 20001;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: min(1320px, calc(100vw - 24px));
                max-width: calc(100vw - 24px);
                max-height: calc(100vh - 18px);
                box-sizing: border-box;
                overflow: visible;
                margin: 0;
                padding: 0;
                background: transparent;
                border: 0;
                border-radius: 0;
                color: #e6edf3;
                font-family: Arial, Verdana, sans-serif;
            }
            #${APP.id}-panel.${APP.id}-hidden { display: none; }
            .${APP.id}-shell {
                position: relative;
                width: min(1260px, calc(100vw - 72px));
                max-width: 100%;
                min-width: 0;
                margin: 0 auto;
                padding: 0;
                overflow: visible;
            }
            .${APP.id}-frame {
                position: relative;
                z-index: 1;
                display: flex;
                flex-direction: column;
                width: 100%;
                max-width: 100%;
                min-width: 0;
                max-height: calc(100vh - 42px);
                border: 2px solid #30363d;
                border-radius: 4px;
                background: #161b22;
                color: #e6edf3;
                overflow: hidden;
            }
            .${APP.id}-head {
                padding: 9px 14px 8px;
                background: linear-gradient(to bottom, #0d1117 0%, #161b22 100%);
                border-bottom: 1px solid #30363d;
            }
            .${APP.id}-head strong {
                display: block;
                color: #c9a84c;
                font-size: 16px;
                line-height: 20px;
                font-weight: bold;
            }
            .${APP.id}-head span {
                display: block;
                margin-top: 2px;
                color: #8b949e;
                font-size: 12px;
                line-height: 15px;
            }
            .${APP.id}-head span span {
                display: inline;
                margin-top: 0;
            }
            .${APP.id}-close {
                position: absolute;
                top: -12px;
                right: -12px;
                z-index: 3;
                width: 20px;
                height: 20px;
                line-height: 16px;
                padding: 0;
                border: 2px solid #30363d;
                border-radius: 2px;
                background: #21262d;
                color: #e6edf3;
                cursor: pointer;
                font-family: Verdana, Arial, sans-serif;
                font-size: 18px;
                font-weight: 700;
                text-align: center;
                box-shadow: 0 1px 3px rgba(0,0,0,.5);
            }
            .${APP.id}-close:hover {
                background: #30363d;
            }
            .${APP.id}-body {
                display: flex;
                flex-direction: column;
                min-height: 0;
                min-width: 0;
                padding: 6px 14px 8px;
                overflow: hidden;
            }
            .${APP.id}-section {
                display: grid;
                grid-template-columns: minmax(240px, 280px) minmax(0, 1fr);
                gap: 8px 18px;
                min-width: 0;
                padding: 8px 0 9px 12px;
                background: transparent;
                border-top: 1px solid #30363d;
                border-bottom: 0;
                border-left: 4px solid #8a6f2e;
            }
            .${APP.id}-section:first-child {
                border-top: 0;
            }
            .${APP.id}-section:last-child {
                padding-bottom: 0;
            }
            .${APP.id}-filters { border-left-color: #f85149; }
            .${APP.id}-settings-section { border-left-color: #a371f7; }
            .${APP.id}-summary-section { border-left-color: #4a9eff; }
            .${APP.id}-list-section {
                border-left-color: #c9a84c;
                min-height: 0;
            }
            .${APP.id}-list-section .${APP.id}-section-options {
                min-height: 0;
                overflow: hidden;
            }
            .${APP.id}-actions-section {
                border-left-color: #8a6f2e;
                border-bottom: 0;
            }
            .${APP.id}-section-title {
                margin: 0 0 3px;
                color: #c9a84c;
                font-size: 13px;
                line-height: 16px;
                font-weight: bold;
                text-transform: uppercase;
            }
            .${APP.id}-section-desc {
                margin: 2px 0 0;
                color: #8b949e;
                font-size: 11px;
                line-height: 14px;
            }
            .${APP.id}-section-options {
                min-width: 0;
            }
            .${APP.id}-toolbar {
                display: grid;
                grid-template-columns: repeat(4, minmax(115px, 1fr));
                gap: 6px 7px;
            }
            .${APP.id}-field {
                display: flex;
                flex-direction: column;
                gap: 4px;
                min-width: 0;
            }
            .${APP.id}-field label {
                color: #e6edf3;
                font-size: 11px;
                font-weight: 700;
            }
            .${APP.id}-field input,
            .${APP.id}-field select {
                width: 100%;
                box-sizing: border-box;
                height: 28px;
                border: 1px solid #30363d;
                border-radius: 2px;
                background: #21262d;
                color: #e6edf3;
                padding: 5px 7px;
                font: 11px Verdana, Arial, sans-serif;
                box-shadow: inset 0 1px 2px rgba(0,0,0,.12);
            }
            .${APP.id}-field input:focus,
            .${APP.id}-field select:focus {
                outline: 2px solid rgba(167,34,30,.25);
                border-color: #c9a84c;
            }
            .${APP.id}-actions {
                display: grid;
                grid-template-columns: 150px minmax(150px, 1fr) minmax(150px, 1fr) minmax(120px, auto);
                align-items: start;
                gap: 8px 10px;
            }
            .${APP.id}-action-stack {
                display: grid;
                gap: 7px;
            }
            .${APP.id}-button {
                min-height: 32px;
                border: 1px solid #0d1117;
                border-radius: 3px;
                background: linear-gradient(to bottom, #c9a84c, #8a6f2e 55%, #0d1117);
                color: #fff;
                cursor: pointer;
                font: bold 11px Verdana, Arial, sans-serif;
                padding: 6px 10px;
                text-shadow: 1px 1px 1px #000;
                box-shadow: inset 0 1px 0 rgba(255,255,255,.25), inset 0 -1px 0 rgba(0,0,0,.3);
                white-space: nowrap;
            }
            .${APP.id}-button:hover {
                background: linear-gradient(to bottom, #e8c873, #c9a84c 55%, #8a6f2e);
            }
            .${APP.id}-button.${APP.id}-brown {
                border-color: #30363d;
                background: linear-gradient(to bottom, #30363d, #21262d 55%, #161b22);
            }
            .${APP.id}-button.${APP.id}-brown:hover {
                background: linear-gradient(to bottom, #3a4048, #30363d 55%, #161b22);
            }
            .${APP.id}-button:disabled {
                opacity: .55;
                cursor: wait;
            }
            .${APP.id}-check {
                flex-direction: row;
                align-items: center;
                gap: 6px;
                color: #e6edf3;
                font-weight: 700;
                min-height: 32px;
            }
            .${APP.id}-check input {
                width: auto;
                height: auto;
                margin: 0;
                accent-color: #f85149;
            }
            .${APP.id}-config-list {
                display: grid;
                grid-template-columns: repeat(2, minmax(0, 1fr));
                column-gap: 18px;
                gap: 0;
            }
            .${APP.id}-config-row {
                display: grid;
                grid-template-columns: 18px minmax(0, 1fr);
                gap: 8px;
                align-items: start;
                padding: 6px 0;
                border-top: 1px solid #30363d;
            }
            .${APP.id}-range-row {
                grid-column: 1 / -1;
            }
            .${APP.id}-config-row:first-child {
                border-top: 0;
                padding-top: 0;
            }
            .${APP.id}-config-row:nth-child(2) {
                border-top: 0;
                padding-top: 0;
            }
            .${APP.id}-config-row input {
                width: 13px;
                height: 13px;
                margin: 2px 0 0;
                accent-color: #f85149;
            }
            .${APP.id}-config-row b {
                display: block;
                color: #e6edf3;
                font-size: 12px;
                line-height: 15px;
            }
            .${APP.id}-config-row span {
                display: block;
                margin-top: 2px;
                color: #8b949e;
                font-size: 11px;
                line-height: 14px;
            }
            .${APP.id}-config-row .${APP.id}-range-head {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 10px;
                margin: 0;
            }
            .${APP.id}-config-row .${APP.id}-range-head span {
                margin: 0;
                color: inherit;
                font: inherit;
                line-height: inherit;
            }
            .${APP.id}-config-row output {
                color: #c9a84c;
                font: bold 12px Verdana, Arial, sans-serif;
            }
            .${APP.id}-config-row input[type="range"] {
                width: 100%;
                height: 16px;
                margin: 3px 0 0;
                accent-color: #c9a84c;
            }
            .${APP.id}-summary {
                display: grid;
                grid-template-columns: repeat(4, 1fr);
                gap: 8px;
            }
            .${APP.id}-metric {
                border: 1px solid #30363d;
                border-radius: 2px;
                background: #21262d;
                padding: 6px 8px;
                min-width: 0;
                box-shadow: inset 0 1px 2px rgba(0,0,0,.08);
            }
            .${APP.id}-metric b {
                display: block;
                font-size: 16px;
                color: #e6edf3;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            .${APP.id}-metric span {
                color: #8a6f2e;
                font-size: 10px;
                font-weight: 700;
                text-transform: uppercase;
            }
            .${APP.id}-content {
                max-width: 100%;
                overflow-x: hidden;
                overflow-y: auto;
                height: clamp(170px, 28vh, 270px);
                max-height: clamp(170px, 28vh, 270px);
                min-height: 0;
                border: 1px solid #30363d;
                background: #161b22;
            }
            .${APP.id}-table {
                width: 100%;
                border-collapse: collapse;
                table-layout: fixed;
            }
            .${APP.id}-table th:nth-child(1),
            .${APP.id}-table td:nth-child(1) { width: 11%; }
            .${APP.id}-table th:nth-child(2),
            .${APP.id}-table td:nth-child(2) { width: 21%; }
            .${APP.id}-table th:nth-child(3),
            .${APP.id}-table td:nth-child(3) { width: 8%; }
            .${APP.id}-table th:nth-child(4),
            .${APP.id}-table td:nth-child(4) { width: 15%; }
            .${APP.id}-table th:nth-child(5),
            .${APP.id}-table td:nth-child(5) { width: 10%; }
            .${APP.id}-table th:nth-child(6),
            .${APP.id}-table td:nth-child(6) { width: 15%; }
            .${APP.id}-table th:nth-child(7),
            .${APP.id}-table td:nth-child(7) { width: 10%; }
            .${APP.id}-table th:nth-child(8),
            .${APP.id}-table td:nth-child(8) { width: 10%; }
            .${APP.id}-table th,
            .${APP.id}-table td {
                border-bottom: 1px solid #30363d;
                padding: 6px 7px;
                text-align: left;
                vertical-align: top;
                overflow: hidden;
                text-overflow: ellipsis;
                color: #e6edf3;
            }
            .${APP.id}-table th {
                position: sticky;
                top: 0;
                z-index: 1;
                background: linear-gradient(to bottom, #21262d, #30363d);
                color: #e6edf3;
                font-size: 10px;
                text-transform: uppercase;
                border-bottom: 1px solid #30363d;
            }
            .${APP.id}-table tr:nth-child(even) td { background: #21262d; }
            .${APP.id}-table tr:nth-child(odd) td { background: #161b22; }
            .${APP.id}-table a {
                color: #c9a84c;
                font-weight: 700;
                text-decoration: none;
            }
            .${APP.id}-table a:hover { text-decoration: underline; }
            .${APP.id}-muted { color: #8b949e; }
            .${APP.id}-pos { color: #3fb950; font-weight: 700; }
            .${APP.id}-neg { color: #f85149; font-weight: 700; }
            .${APP.id}-table td.${APP.id}-muted { color: #8b949e; }
            .${APP.id}-table td.${APP.id}-pos { color: #3fb950; font-weight: 700; }
            .${APP.id}-table td.${APP.id}-neg { color: #f85149; font-weight: 700; }
            .${APP.id}-map-layer {
                position: absolute;
                inset: 0;
                z-index: 90;
                overflow: visible;
                pointer-events: none;
            }
            .${APP.id}-map-marker {
                --${APP.id}-marker-color: #f85149;
                --${APP.id}-marker-rgb: 217,21,47;
                --${APP.id}-marker-bg-alpha: .95;
                --${APP.id}-marker-glow: rgba(217,21,47,.95);
                position: absolute;
                z-index: 80;
                display: flex;
                align-items: center;
                justify-content: center;
                width: 20px;
                height: 20px;
                margin: -10px 0 0 -10px;
                border: 1px solid #e6edf3;
                border-radius: 50%;
                background: rgba(var(--${APP.id}-marker-rgb), var(--${APP.id}-marker-bg-alpha));
                box-shadow: 0 0 0 2px var(--${APP.id}-marker-color), 0 0 9px var(--${APP.id}-marker-glow), inset 0 0 0 1px rgba(91,18,14,.65);
                color: #e6edf3;
                font: bold 9px/20px Verdana, Arial, sans-serif;
                letter-spacing: 0;
                text-align: center;
                text-shadow: 1px 1px 1px #000;
                pointer-events: none;
            }
            .${APP.id}-map-age-1h {
                --${APP.id}-marker-color: #3fb950;
                --${APP.id}-marker-rgb: 24,168,59;
                --${APP.id}-marker-glow: rgba(24,168,59,.95);
            }
            .${APP.id}-map-age-3h {
                --${APP.id}-marker-color: #c9a84c;
                --${APP.id}-marker-rgb: 215,179,22;
                --${APP.id}-marker-glow: rgba(215,179,22,.95);
            }
            .${APP.id}-map-age-6h {
                --${APP.id}-marker-color: #f0883e;
                --${APP.id}-marker-rgb: 238,124,19;
                --${APP.id}-marker-glow: rgba(238,124,19,.95);
            }
            .${APP.id}-map-age-old {
                --${APP.id}-marker-color: #f85149;
                --${APP.id}-marker-rgb: 217,21,47;
                --${APP.id}-marker-glow: rgba(217,21,47,.95);
            }
            #${APP.id}-map-load {
                position: absolute;
                top: 8px;
                right: 8px;
                z-index: 95;
                display: inline-flex;
                align-items: center;
                justify-content: flex-start;
                gap: 8px;
                width: 34px;
                height: 30px;
                min-width: 34px;
                max-width: 200px;
                overflow: hidden;
                white-space: nowrap;
                border: 1px solid #30363d;
                border-radius: 2px;
                background: linear-gradient(to bottom, #c9a84c, #8a6f2e 55%, #0d1117);
                box-shadow: inset 0 1px 0 rgba(255,255,255,.35), inset 0 -1px 0 rgba(0,0,0,.35), 0 2px 5px rgba(0,0,0,.45);
                color: #fff;
                font: bold 12px Verdana, Arial, sans-serif;
                text-shadow: 1px 1px 1px #000;
                padding: 0 7px;
                cursor: pointer;
                transition: width .18s ease, background .12s ease;
            }
            #${APP.id}-map-load:hover,
            #${APP.id}-map-load:focus {
                width: 188px;
                background: linear-gradient(to bottom, #e8c873, #c9a84c 55%, #8a6f2e);
            }
            #${APP.id}-map-load .${APP.id}-map-load-icon {
                flex: 0 0 18px;
                position: relative;
                display: flex;
                align-items: center;
                justify-content: center;
                width: 18px;
                height: 16px;
                font-size: 14px;
                line-height: 1;
                filter: drop-shadow(0 1px 1px rgba(0,0,0,.5));
            }
            #${APP.id}-map-load .${APP.id}-map-load-label {
                flex: 0 0 auto;
                opacity: 0;
                transform: translateX(-4px);
                transition: opacity .12s ease, transform .12s ease;
            }
            #${APP.id}-map-load:hover .${APP.id}-map-load-label,
            #${APP.id}-map-load:focus .${APP.id}-map-load-label {
                opacity: 1;
                transform: translateX(0);
            }
            #${APP.id}-map-load:disabled {
                opacity: .72;
                cursor: wait;
            }
            #${APP.id}-map-load:disabled,
            #${APP.id}-map-load.${APP.id}-map-load-busy {
                width: 138px;
            }
            #${APP.id}-map-load.${APP.id}-map-load-busy .${APP.id}-map-load-label {
                opacity: 1;
                transform: translateX(0);
            }
            #${APP.id}-map-load.${APP.id}-map-load-fixed {
                position: fixed;
                right: 122px;
                bottom: 18px;
                top: auto;
            }
            .${APP.id}-footer {
                display: flex;
                align-items: center;
                justify-content: space-between;
                gap: 10px;
                margin-top: 10px;
                color: #8b949e;
                font-size: 11px;
                line-height: 14px;
            }
            .${APP.id}-footer a {
                color: #c9a84c;
                font-weight: 700;
                text-decoration: none;
            }
            .${APP.id}-footer a:hover {
                text-decoration: underline;
            }
            .${APP.id}-notice {
                padding: 22px;
                color: #e6edf3;
                text-align: center;
                font-weight: 700;
            }
            .${APP.id}-loading {
                opacity: .68;
                pointer-events: none;
            }
            @media (max-width: 920px) {
                #${APP.id}-panel {
                    top: 50%;
                    max-width: calc(100vw - 24px);
                }
                .${APP.id}-section {
                    grid-template-columns: 1fr;
                    gap: 8px;
                }
                .${APP.id}-toolbar { grid-template-columns: repeat(2, minmax(0, 1fr)); }
                .${APP.id}-config-list { grid-template-columns: 1fr; }
                .${APP.id}-range-row { grid-column: auto; }
                .${APP.id}-summary { grid-template-columns: repeat(2, 1fr); }
                .${APP.id}-actions { grid-template-columns: repeat(2, minmax(0, 1fr)); }
                .${APP.id}-action-stack { grid-column: span 2; }
            }
        `;
        document.head.appendChild(style);
    }

    function createLauncher() {
        if (document.getElementById(`${APP.id}-launcher`)) return;
        const button = document.createElement("button");
        button.id = `${APP.id}-launcher`;
        button.type = "button";
        button.title = "Anabole Eroberung";
        button.setAttribute("aria-label", "Anabole Eroberung");
        button.innerHTML = `
            <span class="${APP.id}-launcher-icon" aria-hidden="true">\u{1F4AA}</span>
            <span class="${APP.id}-launcher-text">Anabole Eroberung</span>
        `;
        button.addEventListener("click", openPanel);
        document.body.appendChild(button);
        state.launcher = button;
        scheduleLauncherPosition();
        window.setTimeout(scheduleLauncherPosition, 250);
        window.setTimeout(scheduleLauncherPosition, 1000);
    }

    function scheduleLauncherPosition() {
        if (state.launcherPositionFrame) window.cancelAnimationFrame(state.launcherPositionFrame);
        state.launcherPositionFrame = window.requestAnimationFrame(() => {
            state.launcherPositionFrame = 0;
            positionLauncher();
        });
    }

    function positionLauncher() {
        const button = document.getElementById(`${APP.id}-launcher`);
        if (!button) return;

        const gameLayout =
            document.querySelector("#main_layout td.maincell")
            || document.querySelector("td.maincell")
            || document.querySelector("#contentContainer")
            || document.querySelector("#content_value");

        let left = 12;

        if (gameLayout) {
            const layoutRect = gameLayout.getBoundingClientRect();
            if (layoutRect.width > 0) left = Math.max(4, Math.round(layoutRect.left - 55));
        }

        button.style.setProperty("left", `${left}px`, "important");
        button.style.setProperty("right", "auto", "important");
        button.style.setProperty("top", "370px", "important");
        button.style.setProperty("bottom", "auto", "important");
    }

    function ensureMapLoadButton() {
        const existing = document.getElementById(`${APP.id}-map-load`);
        if (!isMapScreen()) {
            if (existing) existing.remove();
            state.mapLoadButton = null;
            state.mapButtonForceMarkers = false;
            return;
        }

        const parent = findMapOverlayRoot() || document.body;
        let button = existing;
        if (!button) {
            button = document.createElement("button");
            button.id = `${APP.id}-map-load`;
            button.type = "button";
            button.setAttribute("aria-label", "Eroberungen markieren");
            button.title = "Markiert aktuelle Eroberungen auf der Karte";
            button.innerHTML = `
                <span class="${APP.id}-map-load-icon" aria-hidden="true">\u{1F4AA}</span>
                <span class="${APP.id}-map-load-label">Eroberungen markieren</span>
            `;
            button.addEventListener("click", loadConquestsFromMapButton);
        }

        if (parent !== document.body && window.getComputedStyle(parent).position === "static") {
            parent.style.position = "relative";
        }
        if (button.parentElement !== parent) parent.appendChild(button);
        button.classList.toggle(`${APP.id}-map-load-fixed`, parent === document.body);
        state.mapLoadButton = button;
    }

    function setMapLoadButtonBusy(isBusy) {
        if (!state.mapLoadButton) return;
        const label = state.mapLoadButton.querySelector(`.${APP.id}-map-load-label`);
        state.mapLoadButton.disabled = isBusy;
        state.mapLoadButton.classList.toggle(`${APP.id}-map-load-busy`, isBusy);
        state.mapLoadButton.setAttribute("aria-label", isBusy ? "Eroberungen werden geladen" : "Eroberungen markieren");
        state.mapLoadButton.title = isBusy ? "Eroberungen werden geladen..." : "Markiert aktuelle Eroberungen auf der Karte";
        if (label) label.textContent = isBusy ? "Wird geladen..." : "Eroberungen markieren";
    }

    function isMapScreen() {
        const dataScreen = window.game_data && window.game_data.screen;
        if (dataScreen === "map") return true;
        return new URLSearchParams(window.location.search).get("screen") === "map";
    }

    async function loadConquestsFromMapButton() {
        state.mapButtonForceMarkers = true;
        if (hasPanelControls() && state.controls.markMap) state.controls.markMap.checked = true;
        await loadWorldData({ forceMap: false, forceConquer: true });
        scheduleMapMarkers(0);
    }

    function createPanel() {
        const panel = document.createElement("section");
        panel.id = `${APP.id}-panel`;
        panel.className = `${APP.id}-hidden`;
        panel.innerHTML = `
            <div class="${APP.id}-shell">
                <button class="${APP.id}-close" type="button" aria-label="Schliessen">X</button>
                <div class="${APP.id}-frame">
                    <div class="${APP.id}-head">
                        <strong>\u{1F4AA} Anabole Eroberung</strong>
                        <span>Eroberungen, Verluste und Zusammenfassungen der aktuellen Welt. <span id="${APP.id}-world"></span></span>
                    </div>
                    <div class="${APP.id}-body">
                        <div class="${APP.id}-section ${APP.id}-filters">
                            <div class="${APP.id}-section-copy">
                                <div class="${APP.id}-section-title">Eroberungen</div>
                                <p class="${APP.id}-section-desc">Filtert nach Zeitraum, Spieler, Stamm, Dorf, Koordinate und Kontinent.</p>
                            </div>
                            <div class="${APP.id}-section-options">
                                <div class="${APP.id}-toolbar">
                                    <div class="${APP.id}-field">
                                        <label for="${APP.id}-hours">Zeitraum</label>
                                        <select id="${APP.id}-hours">
                                            <option value="1" selected>Letzte 1 Stunde</option>
                                            <option value="3">Letzte 3 Stunden</option>
                                            <option value="6">Letzte 6 Stunden</option>
                                            <option value="12">Letzte 12 Stunden</option>
                                            <option value="24">Letzte 24 Stunden</option>
                                            <option value="48">Letzte 48 Stunden</option>
                                            <option value="72">Letzte 3 Tage</option>
                                            <option value="168">Letzte 7 Tage</option>
                                            <option value="0">Alles in der Datei</option>
                                        </select>
                                    </div>
                                    <div class="${APP.id}-field">
                                        <label for="${APP.id}-side">Filterseite</label>
                                        <select id="${APP.id}-side">
                                            <option value="both" selected>Beide Seiten</option>
                                            <option value="gain">Wer erobert hat</option>
                                            <option value="loss">Wer verloren hat</option>
                                        </select>
                                    </div>
                                    <div class="${APP.id}-field">
                                        <label for="${APP.id}-group">Ansicht</label>
                                        <select id="${APP.id}-group">
                                            <option value="rows" selected>Eroberungen</option>
                                            <option value="player">Nach Spieler gruppieren</option>
                                            <option value="tribe">Nach Stamm gruppieren</option>
                                            <option value="day">Nach Tag gruppieren</option>
                                        </select>
                                    </div>
                                    <div class="${APP.id}-field">
                                        <label for="${APP.id}-sort">Sortieren</label>
                                        <select id="${APP.id}-sort">
                                            <option value="recent" selected>Neueste zuerst</option>
                                            <option value="oldest">Aelteste zuerst</option>
                                            <option value="points">Punkte</option>
                                            <option value="village">Dorf</option>
                                            <option value="winner">Spieler +</option>
                                            <option value="loser">Spieler -</option>
                                            <option value="tribe">Stamm +</option>
                                        </select>
                                    </div>
                                    <div class="${APP.id}-field">
                                        <label for="${APP.id}-search">Suchen</label>
                                        <input id="${APP.id}-search" type="search" placeholder="Spieler, Stamm, Dorf, Koord.">
                                    </div>
                                    <div class="${APP.id}-field">
                                        <label for="${APP.id}-continent">Kontinent</label>
                                        <input id="${APP.id}-continent" type="text" placeholder="K55 oder 55" maxlength="3">
                                    </div>
                                    <div class="${APP.id}-field">
                                        <label for="${APP.id}-limit">Sichtbares Limit</label>
                                        <input id="${APP.id}-limit" type="number" min="20" max="5000" step="10" value="${APP.defaultLimit}">
                                    </div>
                                </div>
                            </div>
                        </div>

                        <div class="${APP.id}-section ${APP.id}-settings-section">
                            <div class="${APP.id}-section-copy">
                                <div class="${APP.id}-section-title">Einstellungen</div>
                                <p class="${APP.id}-section-desc">Legt dauerhafte Filter fest und leert die Ansicht.</p>
                            </div>
                            <div class="${APP.id}-section-options">
                                <div class="${APP.id}-config-list">
                                    <label class="${APP.id}-config-row">
                                        <input id="${APP.id}-hide-barbarians" type="checkbox">
                                        <span>
                                            <b>Barbarendoerfer ausblenden</b>
                                            <span>Entfernt Eroberungen, bei denen Barbaren das Dorf gewonnen oder verloren haben.</span>
                                        </span>
                                    </label>
                                    <label class="${APP.id}-config-row">
                                        <input id="${APP.id}-hide-own" type="checkbox">
                                        <span>
                                            <b>Eigene Eroberungen ausblenden</b>
                                            <span>Entfernt Eroberungen, bei denen der aktuelle Spieler als Gewinner oder Verlierer erscheint.</span>
                                        </span>
                                    </label>
                                    <label class="${APP.id}-config-row">
                                        <input id="${APP.id}-hide-self" type="checkbox">
                                        <span>
                                            <b>Selbst-Eroberungen ausblenden</b>
                                            <span>Entfernt Eroberungen, bei denen derselbe Spieler sein eigenes Dorf zurueckerobert hat.</span>
                                        </span>
                                    </label>
                                    <label class="${APP.id}-config-row">
                                        <input id="${APP.id}-mark-map" type="checkbox">
                                        <span>
                                            <b>Auf der Karte markieren</b>
                                            <span>Markiert auf der Karte die im aktuellen Zeitraum und Filter eroberten Doerfer.</span>
                                        </span>
                                    </label>
                                    <div class="${APP.id}-config-row ${APP.id}-range-row">
                                        <span></span>
                                        <span>
                                            <b class="${APP.id}-range-head">
                                                <span>Hintergrund-Deckkraft</span>
                                                <output id="${APP.id}-map-opacity-value">95%</output>
                                            </b>
                                            <input id="${APP.id}-map-opacity" type="range" min="45" max="100" step="5" value="95">
                                            <span>Steuert nur die Transparenz der Hintergrundfarbe der Markierungen.</span>
                                        </span>
                                    </div>
                                </div>
                            </div>
                        </div>

                        <div class="${APP.id}-section ${APP.id}-summary-section">
                            <div class="${APP.id}-section-copy">
                                <div class="${APP.id}-section-title">Uebersicht</div>
                                <p class="${APP.id}-section-desc">Gesamtzahlen des aktiven Filters zum schnellen Ueberblick.</p>
                            </div>
                            <div class="${APP.id}-section-options">
                                <div id="${APP.id}-summary" class="${APP.id}-summary"></div>
                            </div>
                        </div>

                        <div class="${APP.id}-section ${APP.id}-list-section">
                            <div class="${APP.id}-section-copy">
                                <div class="${APP.id}-section-title">Ergebnisse</div>
                                <p class="${APP.id}-section-desc">Tabelle der Eroberungen oder Zusammenfassung nach Spieler, Stamm oder Tag.</p>
                            </div>
                            <div class="${APP.id}-section-options">
                                <div id="${APP.id}-content" class="${APP.id}-content">
                                    <div class="${APP.id}-notice">Oeffne das Panel und klicke auf Aktualisieren.</div>
                                </div>
                            </div>
                        </div>

                        <div class="${APP.id}-section ${APP.id}-actions-section">
                            <div class="${APP.id}-section-copy">
                                <div class="${APP.id}-section-title">Aktionen</div>
                                <p class="${APP.id}-section-desc">Aktualisiert die Daten der aktuellen Ansicht.</p>
                            </div>
                            <div class="${APP.id}-section-options">
                                <div class="${APP.id}-actions">
                                    <div class="${APP.id}-action-stack">
                                        <button id="${APP.id}-save" class="${APP.id}-button" type="button">Speichern</button>
                                        <button id="${APP.id}-reset-settings" class="${APP.id}-button ${APP.id}-brown" type="button">Einstellungen zuruecksetzen</button>
                                    </div>
                                    <button id="${APP.id}-reload" class="${APP.id}-button" type="button">Aktualisieren</button>
                                    <button id="${APP.id}-clear" class="${APP.id}-button" type="button">Cache leeren</button>
                                    <label class="${APP.id}-field ${APP.id}-check">
                                        <input id="${APP.id}-auto" type="checkbox">
                                        Auto 2-5 min
                                    </label>
                                </div>
                                <div class="${APP.id}-footer">
                                    <span id="${APP.id}-status">Bereit.</span>
                                    <span>Oeffentliche Kartendaten. Staemme = aktueller Stamm des Spielers.</span>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(panel);
        state.panel = panel;
        bindPanelControls(panel);
    }

    function bindPanelControls(root) {
        state.controls = {
            world: root.querySelector(`#${APP.id}-world`),
            hours: root.querySelector(`#${APP.id}-hours`),
            side: root.querySelector(`#${APP.id}-side`),
            group: root.querySelector(`#${APP.id}-group`),
            sort: root.querySelector(`#${APP.id}-sort`),
            search: root.querySelector(`#${APP.id}-search`),
            continent: root.querySelector(`#${APP.id}-continent`),
            limit: root.querySelector(`#${APP.id}-limit`),
            hideBarbarians: root.querySelector(`#${APP.id}-hide-barbarians`),
            hideOwn: root.querySelector(`#${APP.id}-hide-own`),
            hideSelf: root.querySelector(`#${APP.id}-hide-self`),
            markMap: root.querySelector(`#${APP.id}-mark-map`),
            mapOpacity: root.querySelector(`#${APP.id}-map-opacity`),
            mapOpacityValue: root.querySelector(`#${APP.id}-map-opacity-value`),
            auto: root.querySelector(`#${APP.id}-auto`),
            reload: root.querySelector(`#${APP.id}-reload`),
            save: root.querySelector(`#${APP.id}-save`),
            resetSettings: root.querySelector(`#${APP.id}-reset-settings`),
            clear: root.querySelector(`#${APP.id}-clear`),
            summary: root.querySelector(`#${APP.id}-summary`),
            content: root.querySelector(`#${APP.id}-content`),
            status: root.querySelector(`#${APP.id}-status`),
        };

        applyPanelSettings(readPanelSettings() || defaultPanelSettings());
        rememberPanelSettings();

        const close = root.querySelector(`.${APP.id}-close`);
        if (close) close.addEventListener("click", closePanel);
        state.controls.reload.addEventListener("click", () => loadWorldData({ forceMap: false, forceConquer: true }));
        state.controls.save.addEventListener("click", savePanelSettings);
        state.controls.resetSettings.addEventListener("click", resetPanelSettings);
        state.controls.clear.addEventListener("click", clearCacheAndReload);
        state.controls.auto.addEventListener("change", () => {
            rememberPanelSettings();
            syncAutoRefresh(true);
        });

        ["hours", "side", "group", "sort", "search", "continent", "limit", "hideBarbarians", "hideOwn", "hideSelf", "markMap", "mapOpacity"].forEach((name) => {
            const control = state.controls[name];
            const eventName = control.type === "checkbox" ? "change" : control.tagName === "INPUT" ? "input" : "change";
            control.addEventListener(eventName, rememberPanelSettings);
            control.addEventListener(eventName, debounce(() => {
                rememberPanelSettings();
                syncMapOpacityOutput();
                if (name === "hours") {
                    loadWorldData({ forceMap: false, forceConquer: true });
                } else if (name === "mapOpacity") {
                    scheduleMapMarkers(0);
                } else {
                    render();
                }
                scheduleMapMarkers();
            }, name === "search" ? 160 : 0));
        });

        syncAutoRefresh(false);
    }

    function defaultPanelSettings() {
        return {
            hours: "1",
            side: "both",
            group: "rows",
            sort: "recent",
            search: "",
            continent: "",
            limit: String(APP.defaultLimit),
            hideBarbarians: false,
            hideOwn: false,
            hideSelf: false,
            markMap: true,
            mapOpacity: "95",
            auto: false,
        };
    }

    function hasPanelControls() {
        return Boolean(
            state.controls
            && state.controls.hours
            && state.controls.hours.isConnected
            && state.controls.content
            && state.controls.content.isConnected,
        );
    }

    function activePanelSettings() {
        if (hasPanelControls()) {
            rememberPanelSettings();
            return Object.assign(defaultPanelSettings(), state.panelSettingsDraft || {});
        }
        return Object.assign(defaultPanelSettings(), state.panelSettingsDraft || readPanelSettings() || {});
    }

    function panelSettingsKey() {
        return `${APP.id}:settings:${window.location.host}`;
    }

    function readPanelSettings() {
        try {
            const raw = window.localStorage.getItem(panelSettingsKey());
            if (!raw) return null;
            const parsed = JSON.parse(raw);
            if (!parsed || typeof parsed !== "object") return null;
            return parsed;
        } catch (_) {
            return null;
        }
    }

    function collectPanelSettings() {
        return {
            hours: state.controls.hours.value,
            side: state.controls.side.value,
            group: state.controls.group.value,
            sort: state.controls.sort.value,
            search: state.controls.search.value.trim(),
            continent: state.controls.continent.value.trim(),
            limit: String(visibleLimit()),
            hideBarbarians: Boolean(state.controls.hideBarbarians.checked),
            hideOwn: Boolean(state.controls.hideOwn.checked),
            hideSelf: Boolean(state.controls.hideSelf.checked),
            markMap: Boolean(state.controls.markMap.checked),
            mapOpacity: String(markerOpacityPercentFromValue(state.controls.mapOpacity.value)),
            auto: Boolean(state.controls.auto.checked),
        };
    }

    function rememberPanelSettings() {
        if (!hasPanelControls()) return;
        state.panelSettingsDraft = collectPanelSettings();
    }

    function applyPanelSettings(settings) {
        const values = Object.assign(defaultPanelSettings(), settings || {});
        setSelectValue(state.controls.hours, values.hours);
        setSelectValue(state.controls.side, values.side);
        setSelectValue(state.controls.group, values.group);
        setSelectValue(state.controls.sort, values.sort);
        state.controls.search.value = String(values.search || "");
        state.controls.continent.value = String(values.continent || "");
        state.controls.limit.value = String(Math.max(20, Math.min(5000, Number(values.limit) || APP.defaultLimit)));
        state.controls.hideBarbarians.checked = Boolean(values.hideBarbarians);
        state.controls.hideOwn.checked = Boolean(values.hideOwn);
        state.controls.hideSelf.checked = Boolean(values.hideSelf);
        state.controls.markMap.checked = values.markMap !== false;
        state.controls.mapOpacity.value = String(markerOpacityPercentFromValue(values.mapOpacity));
        syncMapOpacityOutput();
        state.controls.auto.checked = Boolean(values.auto);
    }

    function setSelectValue(control, value) {
        const stringValue = String(value);
        const exists = Array.from(control.options).some((option) => option.value === stringValue);
        if (exists) control.value = stringValue;
    }

    function syncMapOpacityOutput() {
        if (!state.controls.mapOpacity || !state.controls.mapOpacityValue) return;
        state.controls.mapOpacityValue.textContent = `${markerOpacityPercentFromValue(state.controls.mapOpacity.value)}%`;
    }

    function markerOpacityPercentFromValue(value) {
        return Math.max(45, Math.min(100, Number(value) || 95));
    }

    function markerOpacityValue(settings = activePanelSettings()) {
        return markerOpacityPercentFromValue(settings.mapOpacity) / 100;
    }

    function savePanelSettings() {
        const settings = collectPanelSettings();
        state.panelSettingsDraft = settings;
        try {
            window.localStorage.setItem(panelSettingsKey(), JSON.stringify(settings));
            notify("Einstellungen gespeichert.", "success");
            setStatus("Einstellungen gespeichert.");
        } catch (_) {
            notify("Die Einstellungen konnten nicht gespeichert werden.", "error");
        }
    }

    function resetPanelSettings() {
        try {
            window.localStorage.removeItem(panelSettingsKey());
        } catch (_) {
            // Storage can be blocked by the browser; defaults still apply in the open panel.
        }

        applyPanelSettings(defaultPanelSettings());
        rememberPanelSettings();
        syncAutoRefresh(false);
        notify("Einstellungen zurueckgesetzt.", "success");
        setStatus("Einstellungen zurueckgesetzt.");

        if (state.rows.length) {
            loadWorldData({ forceMap: false, forceConquer: true });
        } else {
            render();
        }
    }

    function openPanel() {
        if (window.Dialog && typeof window.Dialog.show === "function") {
            openNativeDialogPanel();
            return;
        }

        if (!state.panel || !state.panel.isConnected) createPanel();
        state.controls.world.textContent = ` ${worldKey()} - ${window.location.host}`;
        state.panel.classList.remove(`${APP.id}-hidden`);
        if (!state.rows.length && !state.busy) {
            loadWorldData({ forceMap: false, forceConquer: false });
        }
    }

    function openNativeDialogPanel() {
        const html = getFallbackPanelHtml().replace(
            new RegExp(`<button[^>]*class="${APP.id}-close"[^>]*>\\s*X\\s*<\\/button>\\s*`),
            "",
        );

        window.Dialog.show(APP.dialogId, html);
        const frame = document.querySelector(`.${APP.id}-frame`);
        if (!frame) return;

        expandNativeDialog(frame);
        scheduleDialogRecentering();

        state.panel = frame;
        bindPanelControls(document);
        state.controls.world.textContent = ` ${worldKey()} - ${window.location.host}`;

        if (!state.rows.length && !state.busy) {
            loadWorldData({ forceMap: false, forceConquer: false });
        } else {
            render();
        }
    }

    function expandNativeDialog(frame) {
        const box = findNativeDialogBox(frame);
        const content = frame.closest(".popup_box_content") || (box && box.querySelector(".popup_box_content")) || frame.parentElement;
        const shell = frame.closest(`.${APP.id}-shell`) || frame.parentElement;
        const viewportWidth = document.documentElement.clientWidth || window.innerWidth || 1320;
        const width = Math.min(1320, Math.max(320, viewportWidth - 24));

        if (box) {
            setStyleImportant(box, "position", "fixed");
            setStyleImportant(box, "top", "50%");
            setStyleImportant(box, "left", "50%");
            setStyleImportant(box, "right", "auto");
            setStyleImportant(box, "bottom", "auto");
            setStyleImportant(box, "transform", "translate(-50%, -50%)");
            setStyleImportant(box, "margin", "0");
            setStyleImportant(box, "margin-left", "0");
            setStyleImportant(box, "width", `${width}px`);
            setStyleImportant(box, "max-width", "calc(100vw - 24px)");
            setStyleImportant(box, "max-height", "calc(100vh - 8px)");
            setStyleImportant(box, "box-sizing", "border-box");
            setStyleImportant(box, "overflow", "visible");
            setStyleImportant(box, "z-index", "20002");
        }

        [content, content && content.firstElementChild, shell, frame].filter(Boolean).forEach((node) => {
            setStyleImportant(node, "max-width", "100%");
            setStyleImportant(node, "min-width", "0");
            setStyleImportant(node, "box-sizing", "border-box");
            setStyleImportant(node, "overflow-x", "hidden");
            setStyleImportant(node, "overflow-y", "hidden");
        });

        if (shell) {
            setStyleImportant(shell, "width", "min(1260px, calc(100vw - 58px))");
            setStyleImportant(shell, "margin", "0 auto");
            setStyleImportant(shell, "padding", "0");
            setStyleImportant(shell, "overflow", "visible");
        }
        setStyleImportant(frame, "width", "100%");
        setStyleImportant(frame, "max-height", "calc(100vh - 42px)");
        setStyleImportant(frame, "overflow", "hidden");
    }

    function setStyleImportant(node, name, value) {
        if (!node || !node.style) return;
        node.style.setProperty(name, value, "important");
    }

    function recenterNativeDialog() {
        const frame = document.querySelector(`.${APP.id}-frame`);
        if (frame) expandNativeDialog(frame);
    }

    function scheduleDialogRecentering() {
        [0, 50, 150, 350].forEach((delay) => {
            window.setTimeout(recenterNativeDialog, delay);
        });
    }

    function findNativeDialogBox(frame) {
        const explicit = document.getElementById(`popup_box_${APP.dialogId}`);
        if (explicit) return explicit;

        let node = frame.parentElement;
        let candidate = null;
        while (node && node !== document.body) {
            const id = String(node.id || "");
            const className = String(node.className || "");
            const classes = node.classList ? Array.from(node.classList) : [];
            if (id.indexOf("popup_box_") === 0 || id === "popup_box" || classes.includes("popup_box")) return node;
            if (!candidate && /popup|dialog/i.test(`${id} ${className}`)) candidate = node;
            node = node.parentElement;
        }
        return candidate || frame.parentElement;
    }

    function getFallbackPanelHtml() {
        let panel = document.getElementById(`${APP.id}-panel`);
        if (!panel) {
            createPanel();
            panel = document.getElementById(`${APP.id}-panel`);
        }

        const html = panel.innerHTML;
        panel.remove();
        if (state.panel === panel) state.panel = null;
        state.controls = {};
        return html;
    }

    function closePanel() {
        if (window.Dialog && typeof window.Dialog.close === "function" && state.panel && !state.panel.id) {
            window.Dialog.close(APP.dialogId);
            state.panel = null;
            return;
        }

        if (state.panel) state.panel.classList.add(`${APP.id}-hidden`);
    }

    async function loadWorldData({ forceMap, forceConquer }) {
        if (state.busy) return;
        state.busy = true;
        setBusy(true);
        setStatus("Weltdaten werden geladen...");

        try {
            const settings = activePanelSettings();
            const hours = Number(settings.hours || 24);
            const since = hours > 0 ? Math.floor(Date.now() / 1000) - hours * 3600 : 0;
            const conquerPath = buildConquerPath(hours, since);
            const mapExpired = Date.now() - state.mapsLoadedAt > APP.mapCacheMs;
            const conquerExpired = Date.now() - state.conquestsLoadedAt > APP.conquerCacheMs || conquerPath !== state.lastConquerPath;

            const mapPromise = forceMap || mapExpired || !state.maps.players.size
                ? loadMaps(forceMap)
                : Promise.resolve();
            await mapPromise;

            if (forceConquer || conquerExpired || !state.rows.length) {
                const conquerText = await fetchCachedText(`conquer:${conquerPath}`, conquerPath, APP.conquerCacheMs, forceConquer, false);
                state.rows = parseConquests(conquerText, since);
                state.conquestsLoadedAt = Date.now();
                state.lastConquerPath = conquerPath;
            }

            if (hasPanelControls()) render();
            scheduleMapMarkers();
            setStatus(`Aktualisiert: ${formatDateTime(new Date())}`);
        } catch (error) {
            console.error(`[${APP.id}]`, error);
            if (hasPanelControls()) showNotice(`Fehler beim Laden der Daten: ${error.message || error}`);
            notify(`Fehler beim Laden der Eroberungen: ${error.message || error}`, "error");
            setStatus("Fehler beim Laden der Daten.");
            clearMapMarkers();
        } finally {
            state.busy = false;
            setBusy(false);
        }
    }

    async function loadMaps(force) {
        const [villagesText, playersText, tribesText] = await Promise.all([
            fetchCachedText("villages", "/map/village.txt", APP.mapCacheMs, force, true),
            fetchCachedText("players", "/map/player.txt", APP.mapCacheMs, force, true),
            fetchCachedText("tribes", "/map/ally.txt", APP.mapCacheMs, force, true),
        ]);

        state.maps.villages = parseVillages(villagesText);
        state.maps.players = parsePlayers(playersText);
        state.maps.tribes = parseTribes(tribesText);
        state.mapsLoadedAt = Date.now();
    }

    function buildConquerPath(hours, since) {
        if (hours > 0 && hours <= 24) {
            return `/interface.php?func=get_conquer&since=${since}`;
        }
        return "/map/conquer.txt";
    }

    async function fetchCachedText(name, path, ttlMs, force, allowLocalStorage) {
        const now = Date.now();
        const memory = state.memoryCache.get(name);
        if (!force && memory && now - memory.time < ttlMs) return memory.text;

        const storageKey = `${APP.id}:${window.location.host}:${name}`;
        if (!force && allowLocalStorage) {
            const stored = readStorage(storageKey);
            if (stored && now - stored.time < ttlMs) {
                state.memoryCache.set(name, stored);
                return stored.text;
            }
        }

        const response = await fetch(path, {
            credentials: "same-origin",
            cache: "no-store",
            headers: { "Accept": "text/plain,*/*" },
        });

        if (!response.ok) {
            throw new Error(`${path} (${response.status})`);
        }

        const text = await response.text();
        const entry = { time: now, text };
        state.memoryCache.set(name, entry);

        if (allowLocalStorage && text.length <= APP.maxStoredChars) {
            writeStorage(storageKey, entry);
        }

        return text;
    }

    function readStorage(key) {
        try {
            const raw = window.localStorage.getItem(key);
            if (!raw) return null;
            const parsed = JSON.parse(raw);
            if (!parsed || typeof parsed.text !== "string" || typeof parsed.time !== "number") return null;
            return parsed;
        } catch (_) {
            return null;
        }
    }

    function writeStorage(key, value) {
        try {
            window.localStorage.setItem(key, JSON.stringify(value));
        } catch (_) {
            // Large worlds can exceed localStorage; in-memory cache is enough for the current page.
        }
    }

    function clearCacheAndReload() {
        Object.keys(window.localStorage)
            .filter((key) => key.startsWith(`${APP.id}:${window.location.host}:`))
            .forEach((key) => window.localStorage.removeItem(key));
        state.memoryCache.clear();
        state.mapsLoadedAt = 0;
        state.conquestsLoadedAt = 0;
        notify("Eroberungs-Cache geleert.", "success");
        loadWorldData({ forceMap: true, forceConquer: true });
    }

    function parseVillages(text) {
        const villages = new Map();
        for (const line of splitLines(text)) {
            const cols = line.split(",");
            if (cols.length < 7) continue;
            const id = toInt(cols[0]);
            const x = toInt(cols[2]);
            const y = toInt(cols[3]);
            villages.set(id, {
                id,
                name: decodeTW(cols[1]) || `Dorf #${id}`,
                x,
                y,
                coords: `${x}|${y}`,
                continent: continentFromCoords(x, y),
                playerId: toInt(cols[4]),
                points: toInt(cols[5]),
                bonus: toInt(cols[6]),
            });
        }
        return villages;
    }

    function parsePlayers(text) {
        const players = new Map();
        players.set(0, {
            id: 0,
            name: "Barbaren",
            tribeId: 0,
            villages: 0,
            points: 0,
            rank: 0,
        });

        for (const line of splitLines(text)) {
            const cols = line.split(",");
            if (cols.length < 6) continue;
            const id = toInt(cols[0]);
            players.set(id, {
                id,
                name: decodeTW(cols[1]) || `Spieler #${id}`,
                tribeId: toInt(cols[2]),
                villages: toInt(cols[3]),
                points: toInt(cols[4]),
                rank: toInt(cols[5]),
            });
        }
        return players;
    }

    function parseTribes(text) {
        const tribes = new Map();
        tribes.set(0, {
            id: 0,
            name: "Kein Stamm",
            tag: "-",
            members: 0,
            villages: 0,
            points: 0,
            allPoints: 0,
            rank: 0,
        });

        for (const line of splitLines(text)) {
            const cols = line.split(",");
            if (cols.length < 8) continue;
            const id = toInt(cols[0]);
            tribes.set(id, {
                id,
                name: decodeTW(cols[1]) || `Tribo #${id}`,
                tag: decodeTW(cols[2]) || `#${id}`,
                members: toInt(cols[3]),
                villages: toInt(cols[4]),
                points: toInt(cols[5]),
                allPoints: toInt(cols[6]),
                rank: toInt(cols[7]),
            });
        }
        return tribes;
    }

    function parseConquests(text, since) {
        const rows = [];
        for (const line of splitLines(text)) {
            const cols = line.split(",");
            if (cols.length < 4) continue;

            const villageId = toInt(cols[0]);
            const timestamp = toInt(cols[1]);
            if (since && timestamp < since) continue;

            const newOwnerId = toInt(cols[2]);
            const oldOwnerId = toInt(cols[3]);
            const village = state.maps.villages.get(villageId) || fallbackVillage(villageId);
            const newPlayer = playerById(newOwnerId);
            const oldPlayer = playerById(oldOwnerId);
            const newTribe = tribeById(newPlayer.tribeId);
            const oldTribe = tribeById(oldPlayer.tribeId);

            rows.push({
                villageId,
                timestamp,
                date: new Date(timestamp * 1000),
                newOwnerId,
                oldOwnerId,
                village,
                newPlayer,
                oldPlayer,
                newTribe,
                oldTribe,
                search: "",
            });
        }

        rows.forEach((row) => {
            row.search = fold([
                row.village.name,
                row.village.coords,
                row.village.continent,
                row.newPlayer.name,
                row.oldPlayer.name,
                row.newTribe.name,
                row.newTribe.tag,
                row.oldTribe.name,
                row.oldTribe.tag,
            ].join(" "));
            row.searchGain = fold([
                row.village.name,
                row.village.coords,
                row.village.continent,
                row.newPlayer.name,
                row.newTribe.name,
                row.newTribe.tag,
            ].join(" "));
            row.searchLoss = fold([
                row.village.name,
                row.village.coords,
                row.village.continent,
                row.oldPlayer.name,
                row.oldTribe.name,
                row.oldTribe.tag,
            ].join(" "));
        });

        return rows.sort((a, b) => b.timestamp - a.timestamp);
    }

    function fallbackVillage(id) {
        return {
            id,
            name: `Dorf #${id}`,
            x: 0,
            y: 0,
            coords: "?|?",
            continent: "K??",
            playerId: 0,
            points: 0,
            bonus: 0,
        };
    }

    function playerById(id) {
        if (state.maps.players.has(id)) return state.maps.players.get(id);
        return {
            id,
            name: id ? `Spieler #${id}` : "Barbaren",
            tribeId: 0,
            villages: 0,
            points: 0,
            rank: 0,
        };
    }

    function tribeById(id) {
        if (state.maps.tribes.has(id)) return state.maps.tribes.get(id);
        return {
            id,
            name: id ? `Stamm #${id}` : "Kein Stamm",
            tag: id ? `#${id}` : "-",
            members: 0,
            villages: 0,
            points: 0,
            allPoints: 0,
            rank: 0,
        };
    }

    function render() {
        if (!state.panel) return;
        const rows = getCurrentRows();
        renderSummary(rows);

        const groupMode = state.controls.group.value;
        if (!state.rows.length) {
            showNotice("Keine Daten geladen.");
            clearMapMarkers();
            return;
        }
        if (!rows.length) {
            showNotice("Keine Eroberung mit diesen Filtern gefunden.");
            setStatus("0 Eroberungen mit den aktuellen Filtern.");
            clearMapMarkers();
            return;
        }

        if (groupMode === "rows") {
            renderRows(rows);
        } else {
            renderGroups(rows, groupMode);
        }
        scheduleMapMarkers();
    }

    function getCurrentRows() {
        return getRowsForSettings(activePanelSettings());
    }

    function getRowsForSettings(settings) {
        const values = Object.assign(defaultPanelSettings(), settings || {});
        const query = fold(String(values.search || "").trim());
        const side = values.side || "both";
        const continent = normalizeContinent(values.continent || "");
        const hideBarbarians = Boolean(values.hideBarbarians);
        const hideOwn = Boolean(values.hideOwn);
        const hideSelf = Boolean(values.hideSelf);
        const hours = Number(values.hours || 24);
        const since = hours > 0 ? Math.floor(Date.now() / 1000) - hours * 3600 : 0;

        let rows = state.rows.filter((row) => {
            if (since && row.timestamp < since) return false;
            if (continent && row.village.continent !== continent) return false;
            if (hideBarbarians && isBarbarianConquest(row)) return false;
            if (hideOwn && isOwnPlayerConquest(row)) return false;
            if (hideSelf && isSelfConquest(row)) return false;
            if (!query) return true;
            if (side === "gain") return row.searchGain.includes(query);
            if (side === "loss") return row.searchLoss.includes(query);
            return row.search.includes(query);
        });

        rows = rows.slice().sort((a, b) => {
            switch (values.sort) {
                case "oldest":
                    return a.timestamp - b.timestamp;
                case "points":
                    return b.village.points - a.village.points || b.timestamp - a.timestamp;
                case "village":
                    return a.village.name.localeCompare(b.village.name) || b.timestamp - a.timestamp;
                case "winner":
                    return a.newPlayer.name.localeCompare(b.newPlayer.name) || b.timestamp - a.timestamp;
                case "loser":
                    return a.oldPlayer.name.localeCompare(b.oldPlayer.name) || b.timestamp - a.timestamp;
                case "tribe":
                    return a.newTribe.tag.localeCompare(b.newTribe.tag) || b.timestamp - a.timestamp;
                case "recent":
                default:
                    return b.timestamp - a.timestamp;
            }
        });

        return rows;
    }

    function isBarbarianConquest(row) {
        return row.newOwnerId === 0 || row.oldOwnerId === 0 || row.newPlayer.id === 0 || row.oldPlayer.id === 0;
    }

    function isOwnPlayerConquest(row) {
        const id = ownPlayerId();
        return id > 0 && (row.newOwnerId === id || row.oldOwnerId === id || row.newPlayer.id === id || row.oldPlayer.id === id);
    }

    function isSelfConquest(row) {
        return row.newOwnerId > 0 && row.newOwnerId === row.oldOwnerId;
    }

    function ownPlayerId() {
        const player = window.game_data && window.game_data.player;
        return player && Number(player.id) ? Number(player.id) : 0;
    }

    function markMapEnabled() {
        if (hasPanelControls() && state.controls.markMap) return state.controls.markMap.checked;
        if (state.mapButtonForceMarkers && isMapScreen()) return true;
        const settings = readPanelSettings();
        return !settings || settings.markMap !== false;
    }

    function scheduleMapMarkers(delay = 80) {
        if (state.mapMarkerTimer) window.clearTimeout(state.mapMarkerTimer);
        state.mapMarkerTimer = window.setTimeout(updateMapMarkers, delay);
    }

    function updateMapMarkers() {
        clearMapMarkers();
        if (!markMapEnabled() || !state.rows.length) return;

        const root = findMapRoot();
        if (!root) return;

        const rows = mapMarkerRows().slice().sort((a, b) => b.timestamp - a.timestamp);
        const latestByVillage = new Map();
        rows.forEach((row) => {
            if (!row.village || !row.village.id) return;
            if (!latestByVillage.has(row.village.id)) latestByVillage.set(row.village.id, row);
        });

        let marked = 0;
        const candidates = Array.from(latestByVillage.values()).slice(0, APP.mapMarkerSearchMax);
        for (const row of candidates) {
            if (markMapVillage(root, row)) marked += 1;
            if (marked >= APP.mapMarkerMax) break;
        }
    }

    function clearMapMarkers() {
        document.querySelectorAll(`.${APP.id}-map-layer`).forEach((node) => node.remove());
        document.querySelectorAll(`.${APP.id}-map-marker`).forEach((node) => node.remove());
        document.querySelectorAll(`.${APP.id}-map-marker-host`).forEach((node) => {
            const original = node.dataset ? node.dataset.anabconqPosition : undefined;
            if (original == null || original === "") {
                node.style.removeProperty("position");
            } else {
                node.style.position = original;
            }
            node.classList.remove(`${APP.id}-map-marker-host`);
            if (node.dataset) delete node.dataset.anabconqPosition;
        });
        document.querySelectorAll(`.${APP.id}-map-mark`).forEach((node) => {
            node.classList.remove(`${APP.id}-map-mark`);
            if (node.dataset) delete node.dataset.anabconqMarked;
        });
    }

    function mapMarkerRows() {
        return getRowsForSettings(activePanelSettings());
    }

    function findMapRoot() {
        return findMapOverlayRoot()
            || document.getElementById("map")
            || document.getElementById("map_mover")
            || document.getElementById("map_whole")
            || document.querySelector("#map_container")
            || document.querySelector(".map_container")
            || document.querySelector("[id^='map_']");
    }

    function findMapOverlayRoot() {
        return normalizeMapOverlayRoot(
            document.getElementById("map_wrap")
            || document.querySelector("#map_container")
            || document.querySelector(".map_container")
            || document.getElementById("map"),
        );
    }

    function normalizeMapOverlayRoot(root) {
        if (!root) return null;
        if (["TABLE", "TBODY", "THEAD", "TFOOT", "TR"].includes(root.tagName)) return root.parentElement || root;
        return root;
    }

    function markMapVillage(root, row) {
        const target = findMapVillageElement(root, row);
        if (!target) return false;

        target.classList.add(`${APP.id}-map-mark`);
        if (target.dataset) target.dataset.anabconqMarked = "1";
        addMapMarker(root, target, row);
        return true;
    }

    function findMapVillageElement(root, row) {
        const village = row.village;
        const id = String(village.id);
        const x = String(village.x);
        const y = String(village.y);
        const coords = village.coords;
        const idValue = cssAttr(id);
        const coordsValue = cssAttr(coords);
        const selectors = [
            `#map_village_${cssAttr(`${x}_${y}`)}`,
            `#map_village_${idValue}`,
            `#map_${cssAttr(`${x}_${y}`)}`,
            `#map_cell_${cssAttr(`${x}_${y}`)}`,
            `[data-id="${idValue}"]`,
            `[data-village-id="${idValue}"]`,
            `[data-village="${idValue}"]`,
            `[rel="${idValue}"]`,
            `[data-x="${cssAttr(x)}"][data-y="${cssAttr(y)}"]`,
            `[data-coord="${coordsValue}"]`,
            `[data-coords="${coordsValue}"]`,
            `a[href*="screen=info_village"][href*="id=${idValue}"]`,
            `[title*="${coordsValue}"]`,
            `[data-title*="${coordsValue}"]`,
        ];

        for (const selector of selectors) {
            const found = Array.from(root.querySelectorAll(selector));
            for (const element of found) {
                const target = normalizeMapTarget(element, root);
                if (target && isPlausibleMapTarget(target, root)) return target;
            }
        }
        return null;
    }

    function normalizeMapTarget(element, root) {
        if (!element) return null;
        const tag = element.tagName;
        if (tag === "AREA") return null;

        const childTarget = Array.from(element.querySelectorAll?.("a, img, [id^='map_village_'], .map_village, .village") || [])
            .find((child) => isPlausibleMapTarget(child, root));
        if (childTarget) return childTarget;

        if (isPlausibleMapTarget(element, root)) return element;

        const parentTarget = element.closest?.("[id^='map_village_'], .map_village, .village, a");
        if (parentTarget && root.contains(parentTarget) && isPlausibleMapTarget(parentTarget, root)) return parentTarget;

        return childTarget || element;
    }

    function isPlausibleMapTarget(element, root) {
        if (!element || !root || !root.contains(element)) return false;
        const rect = element.getBoundingClientRect();
        const rootRect = root.getBoundingClientRect();
        if (rect.width < 8 || rect.height < 8 || rect.width > 120 || rect.height > 120) return false;
        if (rect.right < rootRect.left || rect.left > rootRect.right || rect.bottom < rootRect.top || rect.top > rootRect.bottom) return false;
        return true;
    }

    function addMapMarker(root, target, row) {
        const host = markerHostForTarget(root, target);
        if (host) {
            prepareMarkerHost(host);
            appendMarkerToHost(host, target, row);
            return;
        }

        const layer = ensureMapMarkerLayer(root);
        if (!layer) return;

        const targetRect = target.getBoundingClientRect();
        const rootRect = layer.parentElement.getBoundingClientRect();
        const marker = createMapMarker(row);
        marker.style.left = `${targetRect.left - rootRect.left + targetRect.width / 2}px`;
        marker.style.top = `${targetRect.top - rootRect.top + targetRect.height / 2}px`;
        layer.appendChild(marker);
    }

    function markerHostForTarget(root, target) {
        if (!target || !root || !root.contains(target)) return null;
        const invalidHosts = new Set(["AREA", "IMG", "INPUT", "BR", "HR", "TABLE", "TBODY", "THEAD", "TFOOT", "TR"]);
        let host = target;
        while (host && root.contains(host) && invalidHosts.has(host.tagName)) {
            host = host.parentElement;
        }
        if (!host || !root.contains(host) || invalidHosts.has(host.tagName)) return null;
        return host;
    }

    function prepareMarkerHost(host) {
        const style = window.getComputedStyle(host);
        if (style.position !== "static") return;

        if (host.dataset && host.dataset.anabconqPosition == null) {
            host.dataset.anabconqPosition = host.style.position || "";
        }
        host.classList.add(`${APP.id}-map-marker-host`);
        host.style.position = "relative";
    }

    function appendMarkerToHost(host, target, row) {
        const hostRect = host.getBoundingClientRect();
        const targetRect = target.getBoundingClientRect();
        const marker = createMapMarker(row);
        marker.style.left = `${targetRect.left - hostRect.left + targetRect.width / 2}px`;
        marker.style.top = `${targetRect.top - hostRect.top + targetRect.height / 2}px`;
        host.appendChild(marker);
    }

    function createMapMarker(row) {
        const marker = document.createElement("span");
        marker.className = `${APP.id}-map-marker ${markerAgeClass(row)}`;
        marker.textContent = markerAgeText(row);
        marker.style.setProperty(`--${APP.id}-marker-bg-alpha`, String(markerOpacityValue()));
        marker.title = `${row.village.name} (${row.village.coords}) - ${formatDateTime(row.date)} - ${markerAgeLabel(row)}`;
        return marker;
    }

    function markerAgeText(row) {
        const ageHours = conquestAgeHours(row);
        if (ageHours < 1) return "-1";
        if (ageHours < 3) return "-3";
        if (ageHours < 6) return "-6";
        return "+6";
    }

    function markerAgeClass(row) {
        const ageHours = conquestAgeHours(row);
        if (ageHours < 1) return `${APP.id}-map-age-1h`;
        if (ageHours < 3) return `${APP.id}-map-age-3h`;
        if (ageHours < 6) return `${APP.id}-map-age-6h`;
        return `${APP.id}-map-age-old`;
    }

    function markerAgeLabel(row) {
        const ageHours = conquestAgeHours(row);
        if (ageHours < 1) return "weniger als 1h";
        if (ageHours < 3) return "1-3h";
        if (ageHours < 6) return "3-6h";
        return "mehr als 6h";
    }

    function conquestAgeHours(row) {
        return Math.max(0, (Date.now() / 1000 - row.timestamp) / 3600);
    }

    function ensureMapMarkerLayer(root) {
        const overlayRoot = normalizeMapOverlayRoot(root);
        if (!overlayRoot) return null;

        const style = window.getComputedStyle(overlayRoot);
        if (style.position === "static") overlayRoot.style.position = "relative";

        let layer = overlayRoot.querySelector(`#${APP.id}-map-layer`);
        if (!layer) {
            layer = document.createElement("div");
            layer.id = `${APP.id}-map-layer`;
            layer.className = `${APP.id}-map-layer`;
            overlayRoot.appendChild(layer);
        }
        return layer;
    }

    function cssAttr(value) {
        return String(value || "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
    }

    function renderSummary(rows) {
        const winners = new Set();
        const losers = new Set();
        const tribes = new Set();
        let latest = 0;

        rows.forEach((row) => {
            winners.add(row.newOwnerId);
            losers.add(row.oldOwnerId);
            tribes.add(row.newTribe.id);
            tribes.add(row.oldTribe.id);
            latest = Math.max(latest, row.timestamp);
        });

        state.controls.summary.textContent = "";
        state.controls.summary.append(
            metric("Eroberungen", formatNumber(rows.length)),
            metric("Spieler +", formatNumber(winners.size)),
            metric("Spieler -", formatNumber(losers.size)),
            metric("Staemme", formatNumber(tribes.size)),
        );

        if (latest) {
            state.controls.status.textContent = `${formatNumber(rows.length)} Eroberungen. Letzte: ${formatDateTime(new Date(latest * 1000))}.`;
        }
    }

    function metric(label, value) {
        const box = document.createElement("div");
        box.className = `${APP.id}-metric`;
        const valueNode = document.createElement("b");
        valueNode.textContent = value;
        const labelNode = document.createElement("span");
        labelNode.textContent = label;
        box.append(valueNode, labelNode);
        return box;
    }

    function renderRows(rows) {
        const limit = visibleLimit();
        const visibleRows = rows.slice(0, limit);
        const table = document.createElement("table");
        table.className = `${APP.id}-table`;
        table.append(
            thead(["Zeit", "Dorf", "Pkt", "Gewonnen", "Stamm +", "Verloren", "Stamm -", "K"]),
            tbodyRows(visibleRows),
        );
        state.controls.content.textContent = "";
        state.controls.content.appendChild(table);
        setStatus(`Zeigt ${formatNumber(visibleRows.length)} von ${formatNumber(rows.length)} Eroberungen.`);
    }

    function tbodyRows(rows) {
        const body = document.createElement("tbody");
        const fragment = document.createDocumentFragment();
        rows.forEach((row) => {
            const tr = document.createElement("tr");
            tr.append(
                td(formatDateTime(row.date)),
                td(villageLink(row.village)),
                td(formatNumber(row.village.points), "muted"),
                td(playerLink(row.newPlayer), "pos"),
                td(tribeLink(row.newTribe), "pos"),
                td(playerLink(row.oldPlayer), "neg"),
                td(tribeLink(row.oldTribe), "neg"),
                td(row.village.continent),
            );
            fragment.appendChild(tr);
        });
        body.appendChild(fragment);
        return body;
    }

    function renderGroups(rows, mode) {
        const groups = buildGroups(rows, mode);
        const limit = visibleLimit();
        const visibleGroups = groups.slice(0, limit);
        const table = document.createElement("table");
        table.className = `${APP.id}-table`;
        table.append(
            thead(mode === "day"
                ? ["Tag", "Eroberungen", "Punkte", "Letzte"]
                : ["Gruppe", "Gewonnen", "Verloren", "Saldo", "Punkte +", "Punkte -", "Letzte"]),
            tbodyGroups(visibleGroups, mode),
        );
        state.controls.content.textContent = "";
        state.controls.content.appendChild(table);
        setStatus(`Zeigt ${formatNumber(visibleGroups.length)} von ${formatNumber(groups.length)} Gruppen.`);
    }

    function buildGroups(rows, mode) {
        if (mode === "day") {
            const dayGroups = new Map();
            rows.forEach((row) => {
                const key = row.date.toISOString().slice(0, 10);
                const group = ensureGroup(dayGroups, key, formatDay(row.date), null);
                group.gains += 1;
                group.pointsGained += row.village.points;
                group.latest = Math.max(group.latest, row.timestamp);
            });
            return Array.from(dayGroups.values()).sort((a, b) => b.latest - a.latest);
        }

        const side = state.controls.side.value;
        const groups = new Map();
        rows.forEach((row) => {
            if (side === "gain" || side === "both") {
                const entity = mode === "tribe" ? row.newTribe : row.newPlayer;
                const group = ensureGroup(groups, `${mode}:${entity.id}`, labelForEntity(entity, mode), entity);
                group.gains += 1;
                group.pointsGained += row.village.points;
                group.latest = Math.max(group.latest, row.timestamp);
            }

            if (side === "loss" || side === "both") {
                const entity = mode === "tribe" ? row.oldTribe : row.oldPlayer;
                const group = ensureGroup(groups, `${mode}:${entity.id}`, labelForEntity(entity, mode), entity);
                group.losses += 1;
                group.pointsLost += row.village.points;
                group.latest = Math.max(group.latest, row.timestamp);
            }
        });

        return Array.from(groups.values()).sort((a, b) => {
            const countDiff = (b.gains + b.losses) - (a.gains + a.losses);
            return countDiff || b.latest - a.latest || a.label.localeCompare(b.label);
        });
    }

    function ensureGroup(map, key, label, entity) {
        if (!map.has(key)) {
            map.set(key, {
                key,
                label,
                entity,
                gains: 0,
                losses: 0,
                pointsGained: 0,
                pointsLost: 0,
                latest: 0,
            });
        }
        return map.get(key);
    }

    function tbodyGroups(groups, mode) {
        const body = document.createElement("tbody");
        const fragment = document.createDocumentFragment();
        groups.forEach((group) => {
            const tr = document.createElement("tr");
            if (mode === "day") {
                tr.append(
                    td(group.label),
                    td(formatNumber(group.gains)),
                    td(formatNumber(group.pointsGained)),
                    td(formatDateTime(new Date(group.latest * 1000))),
                );
            } else {
                const saldo = group.gains - group.losses;
                const labelNode = group.entity
                    ? (mode === "tribe" ? tribeLink(group.entity) : playerLink(group.entity))
                    : text(group.label);
                tr.append(
                    td(labelNode),
                    td(formatNumber(group.gains), "pos"),
                    td(formatNumber(group.losses), "neg"),
                    td(`${saldo > 0 ? "+" : ""}${formatNumber(saldo)}`, saldo >= 0 ? "pos" : "neg"),
                    td(formatNumber(group.pointsGained), "pos"),
                    td(formatNumber(group.pointsLost), "neg"),
                    td(formatDateTime(new Date(group.latest * 1000))),
                );
            }
            fragment.appendChild(tr);
        });
        body.appendChild(fragment);
        return body;
    }

    function thead(labels) {
        const head = document.createElement("thead");
        const row = document.createElement("tr");
        labels.forEach((label) => {
            const th = document.createElement("th");
            th.textContent = label;
            row.appendChild(th);
        });
        head.appendChild(row);
        return head;
    }

    function td(content, variant) {
        const cell = document.createElement("td");
        if (variant) cell.classList.add(`${APP.id}-${variant}`);
        if (content instanceof Node) {
            cell.appendChild(content);
        } else {
            cell.textContent = content == null ? "" : String(content);
        }
        return cell;
    }

    function text(value) {
        return document.createTextNode(value == null ? "" : String(value));
    }

    function villageLink(village) {
        if (!village.id) return text(village.name);
        const link = document.createElement("a");
        link.href = gameUrl("info_village", village.id);
        link.textContent = `${village.name} (${village.coords})`;
        link.title = `ID ${village.id}`;
        return link;
    }

    function playerLink(player) {
        if (!player.id) return text(player.name);
        const link = document.createElement("a");
        link.href = gameUrl("info_player", player.id);
        link.textContent = player.name;
        link.title = `ID ${player.id}`;
        return link;
    }

    function tribeLink(tribe) {
        if (!tribe.id) return text(tribe.tag || tribe.name);
        const link = document.createElement("a");
        link.href = gameUrl("info_ally", tribe.id);
        link.textContent = tribe.tag || tribe.name;
        link.title = tribe.name;
        return link;
    }

    function gameUrl(screen, id) {
        const villageId = window.game_data && window.game_data.village && window.game_data.village.id;
        const params = new URLSearchParams();
        if (villageId) params.set("village", villageId);
        params.set("screen", screen);
        params.set("id", id);
        return `/game.php?${params.toString()}`;
    }

    function visibleLimit() {
        const value = Number(state.controls.limit.value || APP.defaultLimit);
        return Math.max(20, Math.min(5000, value));
    }

    function showNotice(message) {
        state.controls.content.textContent = "";
        const notice = document.createElement("div");
        notice.className = `${APP.id}-notice`;
        notice.textContent = message;
        state.controls.content.appendChild(notice);
    }

    function setBusy(isBusy) {
        setMapLoadButtonBusy(isBusy);
        if (!state.panel || !state.controls.reload) return;
        state.panel.classList.toggle(`${APP.id}-loading`, isBusy);
        ["reload", "save", "resetSettings", "clear"].forEach((name) => {
            if (state.controls[name]) state.controls[name].disabled = isBusy;
        });
    }

    function setStatus(message) {
        if (state.controls.status) state.controls.status.textContent = message;
    }

    function syncAutoRefresh(showMessage = true) {
        if (state.autoTimer) {
            window.clearTimeout(state.autoTimer);
            state.autoTimer = null;
        }
        if (state.controls.auto.checked) {
            scheduleAutoRefresh();
            if (showMessage) notify("Auto-Aktualisierung aktiv: zufaellig zwischen 2 und 5 Minuten.", "success");
        }
    }

    function scheduleAutoRefresh() {
        if (!state.controls.auto.checked) return;
        const delay = randomBetween(APP.autoRefreshMinMs, APP.autoRefreshMaxMs);
        state.autoTimer = window.setTimeout(async () => {
            state.autoTimer = null;
            if (!state.controls.auto.checked) return;

            if (!state.busy && state.panel && state.panel.isConnected && !state.panel.classList.contains(`${APP.id}-hidden`)) {
                await loadWorldData({ forceMap: false, forceConquer: true });
            }

            scheduleAutoRefresh();
        }, delay);
        setStatus(`Auto-Aktualisierung aktiv. Naechste Pruefung in ${Math.round(delay / 60000)} Min.`);
    }

    function randomBetween(min, max) {
        return Math.floor(min + Math.random() * (max - min + 1));
    }

    function notify(message, type) {
        if (window.UI && typeof window.UI.SuccessMessage === "function" && type === "success") {
            window.UI.SuccessMessage(message);
            return;
        }
        if (window.UI && typeof window.UI.ErrorMessage === "function" && type === "error") {
            window.UI.ErrorMessage(message);
            return;
        }
        console[type === "error" ? "error" : "log"](`[${APP.title}] ${message}`);
    }

    function splitLines(textValue) {
        return String(textValue || "")
            .replace(/\r/g, "")
            .split("\n")
            .map((line) => line.trim())
            .filter(Boolean);
    }

    function toInt(value) {
        const parsed = Number.parseInt(value, 10);
        return Number.isFinite(parsed) ? parsed : 0;
    }

    function decodeTW(value) {
        const raw = String(value || "").replace(/\+/g, " ");
        try {
            return decodeURIComponent(raw);
        } catch (_) {
            return raw;
        }
    }

    function fold(value) {
        return String(value || "")
            .normalize("NFD")
            .replace(/[\u0300-\u036f]/g, "")
            .toLowerCase();
    }

    function continentFromCoords(x, y) {
        if (!x || !y) return "K??";
        return `K${Math.floor(y / 100)}${Math.floor(x / 100)}`;
    }

    function normalizeContinent(value) {
        const cleaned = String(value || "").trim().toUpperCase().replace(/^K/, "");
        if (!cleaned) return "";
        if (!/^\d{2}$/.test(cleaned)) return "";
        return `K${cleaned}`;
    }

    function formatNumber(value) {
        return new Intl.NumberFormat("de-DE").format(value || 0);
    }

    function formatDateTime(date) {
        return new Intl.DateTimeFormat("de-DE", {
            day: "2-digit",
            month: "2-digit",
            hour: "2-digit",
            minute: "2-digit",
            second: "2-digit",
            hour12: false,
        }).format(date);
    }

    function formatDay(date) {
        return new Intl.DateTimeFormat("de-DE", {
            day: "2-digit",
            month: "2-digit",
            year: "numeric",
        }).format(date);
    }

    function labelForEntity(entity, mode) {
        if (!entity) return "-";
        if (mode === "tribe") return entity.tag ? `${entity.tag} - ${entity.name}` : entity.name;
        return entity.name;
    }

    function worldKey() {
        const match = window.location.hostname.match(/^([a-z]{2}\d+)/i);
        return match ? match[1].toUpperCase() : window.location.hostname;
    }

    function debounce(fn, ms) {
        let timer = null;
        return (...args) => {
            if (!ms) {
                fn(...args);
                return;
            }
            window.clearTimeout(timer);
            timer = window.setTimeout(() => fn(...args), ms);
        };
    }

    init();
})();