☰

πŸ’ͺ Anaboler Katta

Katapult-Angriffs-Planer fΓΌr Die StΓ€mme

Questo script non dovrebbe essere installato direttamente. È una libreria per altri script da includere con la chiave // @require https://update.greasyfork.org/scripts/587599/1879981/%F0%9F%92%AA%20Anaboler%20Katta.js

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho giΓ  un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho giΓ  un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         πŸ’ͺ Anaboler Katta
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Katapult-Angriffs-Planer fΓΌr Die StΓ€mme
// @match        https://*.die-staemme.de/game.php*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

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

    if (!window.location.href.includes('screen=overview_villages') ||
        !window.location.href.includes('mode=combined')) return;

    // Warte kurz bis Seite geladen
    setTimeout(init, 500);

    function init() {
        // ─── Styles ───────────────────────────────────────────────────────────
        const style = document.createElement('style');
        style.textContent = `
            #kp-panel {
                background: #f4e4bc;
                border: 2px solid #7a3e00;
                border-radius: 4px;
                margin-bottom: 10px;
                font-family: 'Trebuchet MS', sans-serif;
                font-size: 13px;
                color: #3b1e00;
                width: 100%;
                box-sizing: border-box;
            }
            #kp-header {
                background: #7a3e00;
                color: #f0d080;
                padding: 8px 14px;
                display: flex;
                justify-content: space-between;
                align-items: center;
                cursor: pointer;
                user-select: none;
                border-radius: 2px 2px 0 0;
                font-weight: bold;
                font-size: 14px;
            }
            #kp-header:hover { background: #9a5000; }
            #kp-toggle { font-size: 16px; }
            #kp-body { display: none; }
            #kp-body.open { display: block; }
            .kp-section {
                padding: 10px 14px;
                border-bottom: 1px solid #c8a96e;
            }
            .kp-section label {
                display: block;
                font-weight: bold;
                margin-bottom: 4px;
                font-size: 12px;
            }
            #kp-coords {
                width: 100%;
                height: 60px;
                background: #fffbe8;
                border: 1px solid #c8a96e;
                border-radius: 3px;
                padding: 6px;
                font-size: 12px;
                resize: vertical;
                box-sizing: border-box;
            }
            .kp-troops {
                display: flex;
                gap: 8px;
                flex-wrap: wrap;
                margin-top: 4px;
            }
            .kp-troop {
                display: flex;
                align-items: center;
                gap: 5px;
                background: #fffbe8;
                border: 1px solid #c8a96e;
                border-radius: 3px;
                padding: 4px 8px;
            }
            .kp-troop input[type=number] {
                width: 48px;
                background: #fffbe8;
                border: 1px solid #c8a96e;
                border-radius: 3px;
                padding: 2px 4px;
                font-size: 12px;
            }
            .kp-tpl-btn {
                padding: 3px 10px;
                background: #c8a050;
                color: #3b1e00;
                border: 1px solid #7a3e00;
                border-radius: 3px;
                cursor: pointer;
                font-size: 12px;
            }
            .kp-tpl-btn:hover { background: #e0b860; }
            .kp-tpl-btn.del { background: #8b2000; color: #f0d080; }
            .kp-tpl-btn.del:hover { background: #b03000; }
            .kp-tpl-name {
                padding: 3px 6px;
                border: 1px solid #c8a96e;
                border-radius: 3px;
                background: #fffbe8;
                font-size: 12px;
                width: 130px;
            }
            .kp-primary {
                padding: 6px 16px;
                background: #7a3e00;
                color: #f0d080;
                border: 1px solid #f0d080;
                border-radius: 3px;
                cursor: pointer;
                font-weight: bold;
                font-size: 13px;
            }
            .kp-primary:hover { background: #a05000; }
            .kp-error { color: #8b0000; font-size: 12px; margin-top: 4px; }
            .kp-info { font-size: 11px; color: #7a5a00; margin-top: 3px; }
            #kp-results { padding: 10px 14px; }
            #kp-results h3 {
                color: #7a3e00;
                margin: 0 0 8px 0;
                font-size: 13px;
            }
            .kp-attack-row {
                display: flex;
                align-items: center;
                gap: 8px;
                background: #fffbe8;
                border: 1px solid #c8a96e;
                border-radius: 3px;
                padding: 5px 8px;
                margin-bottom: 5px;
                flex-wrap: wrap;
            }
            .kp-attack-row span { flex: 1; font-size: 12px; min-width: 120px; }
            .kp-attack-btn {
                padding: 4px 10px;
                background: #3a6e00;
                color: #d0f080;
                border: 1px solid #d0f080;
                border-radius: 3px;
                cursor: pointer;
                font-size: 12px;
                white-space: nowrap;
            }
            .kp-attack-btn:hover { background: #508000; }
        `;
        document.head.appendChild(style);

        // ─── Panel einfΓΌgen ───────────────────────────────────────────────────
        const panel = document.createElement('div');
        panel.id = 'kp-panel';
        panel.innerHTML = `
            <div id="kp-header">
                πŸ’ͺ Anaboler Katta
                <span id="kp-toggle">β–Ό</span>
            </div>
            <div id="kp-body">
                <div class="kp-section">
                    <label>Koordinaten (Leerzeichen getrennt):</label>
                    <textarea id="kp-coords" placeholder="564|605 516|619 535|604 ..."></textarea>
                    <div class="kp-info" id="kp-coord-info"></div>
                </div>

                <div class="kp-section">
                    <label>Truppen pro Angriff:</label>
                    <div class="kp-troops" id="kp-troops">
                        ${TROOPS.map(t => `
                            <div class="kp-troop">
                                <input type="checkbox" id="kp-chk-${t.key}">
                                <label for="kp-chk-${t.key}" style="font-weight:normal">${t.label}</label>
                                <input type="number" id="kp-amt-${t.key}" value="1" min="0" max="9999">
                            </div>
                        `).join('')}
                    </div>
                </div>

                <div class="kp-section">
                    <label>Vorlagen:</label>
                    <div id="kp-templates-list" style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:8px;"></div>
                    <div style="display:flex;gap:8px;align-items:center;">
                        <input class="kp-tpl-name" type="text" id="kp-tpl-name" placeholder="Name der Vorlage">
                        <button class="kp-tpl-btn" id="kp-tpl-save">Speichern</button>
                    </div>
                </div>

                <div class="kp-section">
                    <label>Optionen:</label>
                    <div style="display:flex;gap:12px;flex-wrap:wrap;margin-top:4px;">
                        <div class="kp-troop">
                            <label for="kp-max-attacks" style="font-weight:normal">Max. Angriffe pro Dorf</label>
                            <input type="number" id="kp-max-attacks" value="" min="1" max="999" placeholder="unbegrenzt">
                        </div>
                        <div class="kp-troop">
                            <label for="kp-max-dist" style="font-weight:normal">Max. Entfernung</label>
                            <input type="number" id="kp-max-dist" value="" min="1" max="999" placeholder="unbegrenzt">
                        </div>
                    </div>
                </div>

                <div class="kp-section" style="display:flex;align-items:center;gap:12px;">
                    <button class="kp-primary" id="kp-calc">Angriffe berechnen</button>
                    <div class="kp-error" id="kp-error"></div>
                </div>

                <div id="kp-results"></div>
            </div>
        `;

        // In content_value einfΓΌgen (existiert auf jeder TW-Seite)
        const container = document.getElementById('content_value') ||
                          document.getElementById('game_body') ||
                          document.body;
        container.insertBefore(panel, container.firstChild);

        // ─── Events ───────────────────────────────────────────────────────────
        document.getElementById('kp-header').addEventListener('click', () => {
            const body = document.getElementById('kp-body');
            const toggle = document.getElementById('kp-toggle');
            body.classList.toggle('open');
            toggle.textContent = body.classList.contains('open') ? 'β–²' : 'β–Ό';
        });

        document.getElementById('kp-calc').addEventListener('click', calculate);
        document.getElementById('kp-tpl-save').addEventListener('click', saveTemplate);
        document.getElementById('kp-coords').addEventListener('input', updateCoordInfo);

        renderTemplates();
    }

    // ─── Truppen-Definitionen ─────────────────────────────────────────────────
    const TROOPS = [
        { key: 'spear',    label: 'Speer'   },
        { key: 'axe',      label: 'Axt'     },
        { key: 'spy',      label: 'SpΓ€her'  },
        { key: 'ram',      label: 'Ramme'   },
        { key: 'catapult', label: 'Kata'    },
    ];

    const COL_INDEX = { spear: 0, sword: 1, axe: 2, spy: 3, light: 4, heavy: 5, ram: 6, catapult: 7, snob: 8 };

    // ─── Truppen aus Tabelle lesen ────────────────────────────────────────────
    function readTroops() {
        const villages = [];
        const rows = document.querySelectorAll('#combined_table tbody tr.nowrap');
        rows.forEach(row => {
            const link = row.querySelector('td a[href*="village="]');
            if (!link) return;
            const idMatch = link.href.match(/village=(\d+)/);
            if (!idMatch) return;
            const villageId = idMatch[1];
            const villageName = link.textContent.trim();

            // Koordinaten aus Titeltext oder Link
            let vx = 0, vy = 0;
            const coordMatch = villageName.match(/\((\d+)\|(\d+)\)/) ||
                               (link.title || '').match(/\((\d+)\|(\d+)\)/);
            if (coordMatch) { vx = parseInt(coordMatch[1]); vy = parseInt(coordMatch[2]); }

            // Koordinaten aus Zeilen-Link falls nicht im Namen
            if (vx === 0) {
                const allLinks = row.querySelectorAll('a[href*="village="]');
                allLinks.forEach(l => {
                    const m = (l.textContent || '').match(/(\d+)\|(\d+)/);
                    if (m) { vx = parseInt(m[1]); vy = parseInt(m[2]); }
                });
            }

            const unitCells = row.querySelectorAll('td.unit-item');
            const troopData = {};
            TROOPS.forEach(t => {
                const idx = COL_INDEX[t.key];
                const cell = unitCells[idx];
                troopData[t.key] = cell ? parseInt(cell.textContent.trim()) || 0 : 0;
            });

            villages.push({ id: villageId, name: villageName, x: vx, y: vy, troops: troopData });
        });
        return villages;
    }

    // ─── Templates ────────────────────────────────────────────────────────────
    function loadTemplates() {
        try { return JSON.parse(GM_getValue('kp_templates', '{}')) || {}; } catch { return {}; }
    }
    function saveTemplates(t) { GM_setValue('kp_templates', JSON.stringify(t)); }

    function renderTemplates() {
        const container = document.getElementById('kp-templates-list');
        if (!container) return;
        const tpls = loadTemplates();
        const names = Object.keys(tpls);
        if (names.length === 0) {
            container.innerHTML = '<span style="font-size:12px;color:#7a5a00">Keine Vorlagen</span>';
            return;
        }
        container.innerHTML = names.map(n => `
            <button class="kp-tpl-btn" data-tpl="${n}">${n}</button>
            <button class="kp-tpl-btn del" data-del="${n}">βœ•</button>
        `).join('');
        container.querySelectorAll('[data-tpl]').forEach(b =>
            b.addEventListener('click', () => loadTemplate(b.dataset.tpl)));
        container.querySelectorAll('[data-del]').forEach(b =>
            b.addEventListener('click', () => deleteTemplate(b.dataset.del)));
    }

    function saveTemplate() {
        const name = document.getElementById('kp-tpl-name').value.trim();
        if (!name) return;
        const tpls = loadTemplates();
        tpls[name] = { coords: document.getElementById('kp-coords').value, troops: getSelectedTroops() };
        saveTemplates(tpls);
        renderTemplates();
        document.getElementById('kp-tpl-name').value = '';
    }

    function loadTemplate(name) {
        const tpl = loadTemplates()[name];
        if (!tpl) return;
        document.getElementById('kp-coords').value = tpl.coords || '';
        TROOPS.forEach(t => {
            const chk = document.getElementById(`kp-chk-${t.key}`);
            const amt = document.getElementById(`kp-amt-${t.key}`);
            if (chk) chk.checked = t.key in (tpl.troops || {});
            if (amt) amt.value = (tpl.troops || {})[t.key] || 1;
        });
        updateCoordInfo();
    }

    function deleteTemplate(name) {
        const tpls = loadTemplates();
        delete tpls[name];
        saveTemplates(tpls);
        renderTemplates();
    }

    // ─── Hilfe ────────────────────────────────────────────────────────────────
    function updateCoordInfo() {
        const coords = parseCoords();
        const info = document.getElementById('kp-coord-info');
        if (info) info.textContent = coords.length > 0 ? `${coords.length} Koordinaten erkannt` : '';
    }

    function parseCoords() {
        const raw = (document.getElementById('kp-coords').value || '').trim();
        if (!raw) return [];
        return raw.split(/\s+/).filter(c => /^\d+\|\d+$/.test(c)).map(c => {
            const [x, y] = c.split('|').map(Number);
            return { str: c, x, y };
        });
    }

    function getSelectedTroops() {
        const result = {};
        TROOPS.forEach(t => {
            const chk = document.getElementById(`kp-chk-${t.key}`);
            const amt = document.getElementById(`kp-amt-${t.key}`);
            if (chk && chk.checked) result[t.key] = parseInt(amt.value) || 0;
        });
        return result;
    }

    // ─── Berechnen ────────────────────────────────────────────────────────────
    function calculate() {
        const error = document.getElementById('kp-error');
        error.textContent = '';

        const coords = parseCoords();
        if (!coords.length) { error.textContent = 'Keine gΓΌltigen Koordinaten eingegeben.'; return; }

        const troops = getSelectedTroops();
        if (!Object.keys(troops).length) { error.textContent = 'Keine Truppen ausgewΓ€hlt.'; return; }

        const maxAttacksPerVillage = parseInt(document.getElementById('kp-max-attacks').value) || Infinity;
        const maxDist = parseInt(document.getElementById('kp-max-dist').value) || Infinity;

        const villages = readTroops();
        if (!villages.length) { error.textContent = 'Keine DΓΆrfer gefunden.'; return; }

        // Berechne Slots pro Dorf und expandiere in einzelne Slot-EintrΓ€ge
        const villageSlots = [];
        villages.forEach(v => {
            if (v.x === 0 && v.y === 0) return;
            const slotsByTroops = Math.min(...Object.entries(troops).map(([k, amt]) =>
                amt > 0 ? Math.floor((v.troops[k] || 0) / amt) : Infinity
            ));
            const slots = Math.min(slotsByTroops, maxAttacksPerVillage);
            for (let i = 0; i < slots; i++) villageSlots.push({ ...v });
        });

        if (!villageSlots.length) { error.textContent = 'Kein Dorf hat genug Truppen.'; return; }

        // Baue alle gΓΌltigen Paare (slot, coord) mit Distanz
        const pairs = [];
        villageSlots.forEach((v, vi) => {
            coords.forEach((c, ci) => {
                const dist = Math.sqrt((v.x - c.x) ** 2 + (v.y - c.y) ** 2);
                if (dist <= maxDist) pairs.push({ vi, ci, dist });
            });
        });

        // Sortiere nach Distanz aufsteigend
        pairs.sort((a, b) => a.dist - b.dist);

        // Greedy Zuweisung: jeder Slot und jede Koordinate nur einmal
        const usedSlots = new Set();
        const usedCoords = new Set();
        const assigned = [];

        pairs.forEach(({ vi, ci, dist }) => {
            if (usedSlots.has(vi) || usedCoords.has(ci)) return;
            usedSlots.add(vi);
            usedCoords.add(ci);
            assigned.push({ vi, ci, dist });
        });

        // 2-opt Verbesserung: tausche wenn Gesamtdistanz sinkt
        let improved = true;
        while (improved) {
            improved = false;
            for (let a = 0; a < assigned.length; a++) {
                for (let b = a + 1; b < assigned.length; b++) {
                    const ra = assigned[a];
                    const rb = assigned[b];
                    const va = villageSlots[ra.vi];
                    const vb = villageSlots[rb.vi];
                    const ca = coords[ra.ci];
                    const cb = coords[rb.ci];
                    const distAB = Math.sqrt((va.x - cb.x) ** 2 + (va.y - cb.y) ** 2);
                    const distBA = Math.sqrt((vb.x - ca.x) ** 2 + (vb.y - ca.y) ** 2);
                    if (distAB <= maxDist && distBA <= maxDist &&
                        distAB + distBA < ra.dist + rb.dist - 0.001) {
                        assigned[a] = { vi: ra.vi, ci: rb.ci, dist: distAB };
                        assigned[b] = { vi: rb.vi, ci: ra.ci, dist: distBA };
                        improved = true;
                    }
                }
            }
        }

        const attacks = assigned.map(({ vi, ci, dist }) => ({
            village: villageSlots[vi],
            target: coords[ci],
            dist: dist.toFixed(1)
        }));

        attacks.sort((a, b) => a.village.id.localeCompare(b.village.id) || a.dist - b.dist);
        renderResults(attacks, troops);
    }

    function renderResults(attacks, troops) {
        const results = document.getElementById('kp-results');
        if (!attacks.length) {
            results.innerHTML = '<div class="kp-error" style="padding:8px">Keine Angriffe berechnet.</div>';
            return;
        }
        const troopStr = Object.entries(troops).map(([k, v]) =>
            `${TROOPS.find(t => t.key === k).label}: ${v}`).join(' Β· ');

        results.innerHTML = `
            <div style="margin-bottom:8px;">
                <button id="kp-send-all" style="background:#5a7a00;color:#f0d090;border:1px solid #8aaa20;padding:6px 18px;font-size:14px;font-weight:bold;cursor:pointer;border-radius:3px;">
                    βš” Alle ${attacks.length} Angriffe senden
                </button>
                <span id="kp-send-status" style="margin-left:12px;color:#c8b464;font-size:13px;"></span>
            </div>
            <h3>βš” ${attacks.length} Angriffe β€” ${troopStr}</h3>
            ${attacks.map((a, i) => `
                <div class="kp-attack-row" id="kp-row-${i}">
                    <span><b>${i + 1}.</b> ${a.village.name} β†’ ${a.target.str} <small>(${a.dist} Felder)</small></span>
                    <button class="kp-attack-btn" data-idx="${i}">Angriff senden βš”</button>
                </div>
            `).join('')}
        `;

        // Einzelne Buttons
        results.querySelectorAll('.kp-attack-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const idx = parseInt(btn.dataset.idx);
                const a = attacks[idx];
                btn.disabled = true;
                btn.textContent = '⏳ Sende…';
                sendAttack(a.village.id, a.target, troops)
                    .then(() => {
                        btn.textContent = 'βœ“ Gesendet';
                        btn.style.background = '#1a4a00';
                        setTimeout(() => document.getElementById(`kp-row-${idx}`)?.remove(), 800);
                    })
                    .catch(err => {
                        btn.disabled = false;
                        btn.textContent = 'βœ— Fehler – erneut?';
                        btn.style.background = '#8b0000';
                        btn.title = String(err);
                    });
            });
        });

        // Alle senden Button
        document.getElementById('kp-send-all').addEventListener('click', async function() {
            this.disabled = true;
            this.textContent = '⏳ Wird gesendet…';
            const status = document.getElementById('kp-send-status');
            let sent = 0, failed = 0;

            for (let i = 0; i < attacks.length; i++) {
                const row = document.getElementById(`kp-row-${i}`);
                if (!row) continue; // bereits gesendet/entfernt
                const btn = row.querySelector('.kp-attack-btn');
                btn.disabled = true;
                btn.textContent = '⏳ Sende…';
                status.textContent = `${i + 1} / ${attacks.length}…`;

                try {
                    await sendAttack(attacks[i].village.id, attacks[i].target, troops);
                    btn.textContent = 'βœ“ Gesendet';
                    btn.style.background = '#1a4a00';
                    sent++;
                    setTimeout(() => row.remove(), 800);
                } catch(err) {
                    btn.disabled = false;
                    btn.textContent = 'βœ— Fehler';
                    btn.style.background = '#8b0000';
                    btn.title = String(err);
                    failed++;
                }

                // ZufΓ€lliger Delay 500–700ms zwischen Angriffen
                if (i < attacks.length - 1) {
                    await new Promise(r => setTimeout(r, 500 + Math.random() * 200));
                }
            }

            this.textContent = `βœ“ Fertig (${sent} gesendet${failed ? ', ' + failed + ' Fehler' : ''})`;
            status.textContent = '';
        });
    }

    function getPlaceForm(villageId) {
        return new Promise((resolve, reject) => {
            const base = `${window.location.origin}/game.php`;
            const iframe = document.createElement('iframe');
            iframe.style.cssText = 'display:none;width:0;height:0;border:0;';
            iframe.src = `${base}?village=${villageId}&screen=place`;
            iframe.onload = () => {
                try {
                    const doc = iframe.contentDocument || iframe.contentWindow.document;
                    let csrfField = null, csrfValue = null;
                    doc.querySelectorAll('input[type=hidden]').forEach(inp => {
                        if (/^[0-9a-f]{15,}$/.test(inp.name)) {
                            csrfField = inp.name;
                            csrfValue = inp.value;
                        }
                    });
                    document.body.removeChild(iframe);
                    if (csrfField) resolve({ csrfField, csrfValue });
                    else reject(new Error('CSRF-Formularfeld nicht im iframe gefunden'));
                } catch(e) {
                    document.body.removeChild(iframe);
                    reject(e);
                }
            };
            iframe.onerror = () => { document.body.removeChild(iframe); reject(new Error('iframe-Fehler')); };
            document.body.appendChild(iframe);
        });
    }

    async function sendAttack(villageId, target, troops) {
        const base = `${window.location.origin}/game.php`;
        const gd = (typeof unsafeWindow !== 'undefined' ? unsafeWindow : window).game_data;
        const csrf = gd && gd.csrf;
        if (!csrf) throw new Error('CSRF-Token nicht gefunden');

        // Schritt 1: Versammlungsplatz in verstecktem iframe laden β†’ Formular-Token lesen
        const { tokenField, tokenValue } = await new Promise((resolve, reject) => {
            const iframe = document.createElement('iframe');
            iframe.style.cssText = 'display:none;width:0;height:0;border:0;';
            iframe.src = `${base}?village=${villageId}&screen=place`;
            iframe.onload = () => {
                try {
                    const iDoc = iframe.contentDocument || iframe.contentWindow.document;
                    let tokenField = null, tokenValue = null;
                    iDoc.querySelectorAll('input[type=hidden]').forEach(inp => {
                        if (/^[0-9a-f]{10,}$/.test(inp.name)) {
                            tokenField = inp.name;
                            tokenValue = inp.value;
                        }
                    });
                    document.body.removeChild(iframe);
                    if (tokenField) resolve({ tokenField, tokenValue });
                    else reject(new Error('Formular-Token nicht im iframe gefunden. Hidden inputs: ' + [...iDoc.querySelectorAll('input[type=hidden]')].map(i=>i.name).join(',')));
                } catch(e) {
                    document.body.removeChild(iframe);
                    reject(e);
                }
            };
            iframe.onerror = () => { document.body.removeChild(iframe); reject(new Error('iframe Ladefehler')); };
            document.body.appendChild(iframe);
        });

        // Schritt 2: POST β†’ BestΓ€tigungsseite
        const step2params = new URLSearchParams({
            source_village: villageId,
            x: target.x,
            y: target.y,
            target_type: 'coord',
            input: `${target.x}|${target.y}`,
            attack: 'Angreifen',
            template_id: '',
        });
        step2params.set(tokenField, tokenValue);
        ['spear','sword','axe','archer','spy','light','marcher','heavy','ram','catapult','snob'].forEach(k => {
            step2params.set(k, troops[k] != null ? String(troops[k]) : '');
        });

        const step2html = await fetch(`${base}?village=${villageId}&screen=place&try=confirm&h=${csrf}`, {
            method: 'POST',
            credentials: 'include',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: step2params.toString(),
        }).then(r => r.text());

        // Schritt 3: hidden inputs aus BestΓ€tigungsseite sammeln + abschicken
        const step3params = new URLSearchParams();
        for (const [, name, value] of step2html.matchAll(/name="([^"]+)"\s+value="([^"]*)"/g)) {
            step3params.set(name, value);
        }
        if (!step3params.has('ch')) throw new Error('BestΓ€tigungs-Token (ch) nicht gefunden (Schritt 2)');

        // Fehlende Pflichtfelder ergΓ€nzen
        step3params.set('h', csrf);
        step3params.set('cb', 'troop_confirm_submit');
        step3params.set('submit_confirm', 'Angreifen');
        if (!step3params.has('attack_name')) step3params.set('attack_name', '');
        if (!step3params.has('building')) step3params.set('building', 'main');

        const step3res = await fetch(`${base}?village=${villageId}&screen=place&action=command`, {
            method: 'POST',
            credentials: 'include',
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Referer': `${base}?village=${villageId}&screen=place&try=confirm`,
            },
            body: step3params.toString(),
        });
        if (!step3res.ok) throw new Error(`HTTP ${step3res.status} (Schritt 3)`);
    }

})();