☰

πŸ’ͺ Anaboler Katta

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

Dit script moet niet direct worden geΓ―nstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/587599/1879981/%F0%9F%92%AA%20Anaboler%20Katta.js

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==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)`);
    }

})();