Katapult-Angriffs-Planer fΓΌr Die StΓ€mme
Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek fΓΌr andere Skripte, welche ΓΌber folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyfork.org/scripts/587599/1879981/%F0%9F%92%AA%20Anaboler%20Katta.js
// ==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)`);
}
})();