您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Fügt einen Button im Spielermenü hinzu, um ausgewählten Wachen (filterbar nach Name, Typ, Leitstelle & Grafikset-Name) ein Grafikset zuzuweisen. Robusteres Laden der Set-Liste.
// ==UserScript== // @name LSS Grafikset-Manager // @namespace http://tampermonkey.net/ // @version 9.1 // @description Fügt einen Button im Spielermenü hinzu, um ausgewählten Wachen (filterbar nach Name, Typ, Leitstelle & Grafikset-Name) ein Grafikset zuzuweisen. Robusteres Laden der Set-Liste. // @author Gemini & User-Input // @match https://www.leitstellenspiel.de/ // @grant none // ==/UserScript== (function() { 'use strict'; // --- CACHES & KONSTANTEN --- let buildingsCache = []; let graphicSetsCache = []; let leitstellenCache = []; const LEITSTELLEN_TYPES = [7]; // Nur Gebäudetyp 7 ist eine Leitstelle const BUILDING_TYPE_MAP = { 0: "Feuerwache", 1: "Feuerwehrschule", 2: "Rettungswache", 3: "Rettungsschule", 4: "Krankenhaus", 5: "Rettungshubschrauber-Station", 6: "Polizeiwache", 7: "Leitstelle", 8: "Polizeischule", 9: "THW", 10: "THW Bundesschule", 11: "Bereitschaftspolizei", 12: "Schnelleinsatzgruppe (SEG)", 13: "Polizeihubschrauberstation", 14: "Bereitstellungsraum", 15: "Wasserrettung", 17: "Polizei-Sondereinheiten", 18: "Feuerwache (Kleinwache)", 19: "Polizeiwache (Kleinwache)", 20: "Rettungswache (Kleinwache)", 21: "Rettungshundestaffel", 24: "Reiterstaffel", 25: "Bergrettungswache", 26: "Seenotrettungswache", 27: "Schule für Seefahrt und Seenotrettung", 28: "Hubschrauberstation (Seenotrettung)" }; // --- HILFSFUNKTIONEN --- const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // --- DATENABRUF --- async function fetchBuildings() { if (buildingsCache.length > 0) return; const response = await fetch('/api/buildings'); if (!response.ok) throw new Error("Wachenliste konnte nicht geladen werden."); buildingsCache = await response.json(); } function extractLeitstellenFromCache() { if (leitstellenCache.length > 0) return leitstellenCache; const leitstellenMap = new Map(); const allLeitstellenBuildings = buildingsCache.filter(b => LEITSTELLEN_TYPES.includes(b.building_type)); for (const lst of allLeitstellenBuildings) { leitstellenMap.set(lst.id, lst.caption); } const usedLeitstellenIds = [...new Set(buildingsCache.map(b => b.leitstelle_building_id).filter(id => id))]; const leitstellen = usedLeitstellenIds.map(id => ({ id: id, name: leitstellenMap.get(id) || `Unbekannte LST (${id})` })); leitstellen.sort((a, b) => a.name.localeCompare(b.name)); leitstellenCache = leitstellen; return leitstellenCache; } async function fetchAndParseGraphicSets() { if (graphicSetsCache.length > 0) return graphicSetsCache; if (buildingsCache.length === 0) await fetchBuildings(); if (buildingsCache.length === 0) throw new Error("Du besitzt keine Gebäude, um die Grafikset-Liste auszulesen."); console.log("Starte Suche nach einer geeigneten Wache zum Auslesen der Grafiksets..."); for (const building of buildingsCache) { try { const response = await fetch(`/buildings/${building.id}/edit`); if (!response.ok) { console.log(`Versuch bei Wache ${building.id} (${building.caption}) fehlgeschlagen (Status: ${response.status}). Versuche nächste...`); continue; } const htmlText = await response.text(); const doc = new DOMParser().parseFromString(htmlText, 'text/html'); const setLinks = doc.querySelectorAll('a.graphic_set_change'); if (setLinks.length > 1) { // Mehr als nur "-Keine-" console.log(`Erfolgreich! Grafiksets von Wache ${building.id} (${building.caption}) ausgelesen.`); const sets = Array.from(setLinks) .map(a => ({ id: a.getAttribute('vehicle_graphic_set_id'), name: a.getAttribute('caption') })) .filter(s => s.id !== ""); sets.sort((a, b) => a.name.localeCompare(b.name)); graphicSetsCache = sets; return graphicSetsCache; } else { console.log(`Wache ${building.id} (${building.caption}) hat keine Grafikset-Liste. Versuche nächste...`); } } catch (error) { console.error(`Fehler bei Wache ${building.id} (${building.caption}):`, error); } } // Wenn die Schleife durchläuft, ohne etwas zu finden throw new Error("Konnte bei keiner deiner Wachen eine Grafikset-Liste finden."); } // --- UI-LOGIK --- function populateBuildingList() { const nameFilter = document.getElementById('lss-gsa-filter-name').value.toLowerCase(); const typeFilter = document.getElementById('lss-gsa-filter-type').value; const lstFilter = document.getElementById('lss-gsa-filter-lst').value; const buildingList = document.getElementById('lss-gsa-building-list'); let filteredBuildings = buildingsCache.filter(b => !LEITSTELLEN_TYPES.includes(b.building_type)); if (nameFilter) { filteredBuildings = filteredBuildings.filter(b => b.caption.toLowerCase().includes(nameFilter)); } if (typeFilter !== 'all') { filteredBuildings = filteredBuildings.filter(b => b.building_type == typeFilter); } if (lstFilter !== 'all') { filteredBuildings = filteredBuildings.filter(b => b.leitstelle_building_id == lstFilter); } buildingList.innerHTML = filteredBuildings.map(b => ` <div> <input type="checkbox" id="building_${b.id}" value="${b.id}" data-name="${b.caption.replace(/"/g, '"')}"> <label for="building_${b.id}">${b.caption}</label> </div> `).join('') || '<div style="text-align: center; color: #888;">Keine Wachen für diesen Filter gefunden.</div>'; } function populateGraphicSetList() { const setFilter = document.getElementById('lss-gsa-filter-set').value.toLowerCase(); const setSelect = document.getElementById('lss-gsa-set-select'); const filteredSets = graphicSetsCache.filter(s => s.name.toLowerCase().includes(setFilter)); setSelect.innerHTML = '<option value="">-- Bitte Grafikset wählen --</option>' + filteredSets .map(s => `<option value="${s.id}">${s.name}</option>`) .join(''); } async function openModal() { const modal = document.getElementById('lss-gsa-modal'); const buildingList = document.getElementById('lss-gsa-building-list'); const setSelect = document.getElementById('lss-gsa-set-select'); const typeFilterSelect = document.getElementById('lss-gsa-filter-type'); const lstFilterSelect = document.getElementById('lss-gsa-filter-lst'); const log = document.getElementById('lss-gsa-log'); const statusDiv = document.getElementById('lss-gsa-status'); modal.style.display = 'flex'; log.innerHTML = ''; statusDiv.innerText = 'Lade Daten...'; buildingList.innerHTML = ''; setSelect.innerHTML = '<option>...</option>'; typeFilterSelect.innerHTML = '<option>...</option>'; lstFilterSelect.innerHTML = '<option>...</option>'; document.getElementById('lss-gsa-filter-name').value = ''; document.getElementById('lss-gsa-filter-set').value = ''; try { statusDiv.innerText = 'Lade Gebäudeliste...'; await fetchBuildings(); statusDiv.innerText = 'Lade Grafiksets...'; const graphicSetsPromise = fetchAndParseGraphicSets(); const leitstellen = extractLeitstellenFromCache(); await graphicSetsPromise; statusDiv.innerText = 'Filter und Listen werden erstellt...'; const usedTypes = [...new Set(buildingsCache.map(b => b.building_type).filter(t => !LEITSTELLEN_TYPES.includes(t)))]; typeFilterSelect.innerHTML = '<option value="all">-- Alle Typen --</option>' + usedTypes .map(typeId => ({ id: typeId, name: BUILDING_TYPE_MAP[typeId] || `Typ ${typeId}` })) .sort((a,b) => a.name.localeCompare(b.name)) .map(type => `<option value="${type.id}">${type.name}</option>`) .join(''); lstFilterSelect.innerHTML = '<option value="all">-- Alle Leitstellen --</option>' + leitstellen .map(lst => `<option value="${lst.id}">${lst.name}</option>`) .join(''); populateGraphicSetList(); populateBuildingList(); statusDiv.innerText = 'Bereit.'; } catch (error) { statusDiv.innerHTML = `<span style="color: red;">Fehler: ${error.message}</span>`; console.error("Fehler im Grafikset-Manager:", error); } } function closeModal() { document.getElementById('lss-gsa-modal').style.display = 'none'; } // --- KERNPROZESS --- async function startUpdateProcess() { const log = document.getElementById('lss-gsa-log'); const assignBtn = document.getElementById('lss-gsa-assign-btn'); const closeBtn = document.getElementById('lss-gsa-close-btn'); log.innerHTML = ''; assignBtn.disabled = true; closeBtn.disabled = true; const selectedSetId = document.getElementById('lss-gsa-set-select').value; const checkedBuildings = Array.from(document.querySelectorAll('#lss-gsa-building-list input[type="checkbox"]:checked')) .map(cb => ({ id: cb.value, name: cb.dataset.name })); if (!selectedSetId) { log.innerHTML = '<span style="color: red;">Fehler: Kein Grafikset ausgewählt.</span>'; } else if (checkedBuildings.length === 0) { log.innerHTML = '<span style="color: red;">Fehler: Keine Wachen ausgewählt.</span>'; } else { let count = 0, successCount = 0, errorCount = 0; log.innerHTML = `Starte Zuweisung für ${checkedBuildings.length} Wachen...\n`; for (const building of checkedBuildings) { count++; log.innerHTML += `(${count}/${checkedBuildings.length}) >> Bearbeite "${building.name}"...\n`; log.scrollTop = log.scrollHeight; try { const editResponse = await fetch(`/buildings/${building.id}/edit`); if (!editResponse.ok) throw new Error(`Edit-Seite nicht erreichbar (Status: ${editResponse.status})`); const editText = await editResponse.text(); const doc = new DOMParser().parseFromString(editText, 'text/html'); const form = doc.querySelector(`form#edit_building_${building.id}`); if (!form) throw new Error("Formular auf der Edit-Seite nicht gefunden."); const formData = new FormData(form); formData.set('building[vehicle_graphic_id]', selectedSetId); const updateResponse = await fetch(`/buildings/${building.id}`, { method: 'POST', body: formData, }); if (updateResponse.ok) { successCount++; log.innerHTML += ` - ✅ Erfolgreich gespeichert.\n`; } else { throw new Error(`Speichern fehlgeschlagen (Status: ${updateResponse.status})`); } } catch (e) { errorCount++; log.innerHTML += `<span style="color: red;"> - ❌ Fehler: ${e.message}</span>\n`; } log.scrollTop = log.scrollHeight; await sleep(1000); } log.innerHTML += `\n🎉 Fertig! ${successCount} erfolgreich, ${errorCount} fehlerhaft.\nBitte die Seite neu laden, um Änderungen zu sehen.`; log.scrollTop = log.scrollHeight; } assignBtn.disabled = false; closeBtn.disabled = false; } // --- UI-ERSTELLUNG UND INITIALISIERUNG --- function setupUI() { const styles = ` #lss-gsa-modal { display: none; justify-content: center; align-items: center; position: fixed; z-index: 9999; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); } #lss-gsa-content { background-color: #f4f4f4; color: #333; padding: 20px; border-radius: 5px; width: 90%; max-width: 800px; height: 90%; max-height: 700px; display: flex; flex-direction: column; box-shadow: 0 5px 15px rgba(0,0,0,0.3); } #lss-gsa-content h2 { margin-top: 0; border-bottom: 1px solid #ccc; padding-bottom: 10px; } #lss-gsa-status { font-style: italic; color: #555; height: 20px; } #lss-gsa-filters { display: grid; grid-template-columns: auto 1fr auto 1fr; gap: 5px 10px; align-items: center; margin-bottom: 10px; font-size: 12px; } #lss-gsa-filters label { font-weight: bold; } #lss-gsa-filters select, #lss-gsa-filters input { padding: 4px; width: 100%; box-sizing: border-box; } #lss-gsa-building-list { border: 1px solid #ccc; background: white; padding: 10px; overflow-y: auto; flex-grow: 1; margin: 10px 0; font-size: 13px; } #lss-gsa-building-list div { display: flex; align-items: center; padding: 2px 0; } #lss-gsa-building-list input { margin-right: 8px; } #lss-gsa-controls { display: flex; gap: 10px; align-items: center; } #lss-gsa-set-select { flex-grow: 1; padding: 5px; } #lss-gsa-log { background-color: #222; color: #eee; font-family: monospace; font-size: 12px; padding: 10px; border-radius: 3px; height: 120px; overflow-y: auto; white-space: pre-wrap; margin: 10px 0; } #lss-gsa-footer { margin-top: auto; text-align: right; } `; const styleSheet = document.createElement("style"); styleSheet.innerText = styles; document.head.appendChild(styleSheet); const modalHTML = ` <div id="lss-gsa-modal"> <div id="lss-gsa-content"> <div style="display: flex; justify-content: space-between; align-items: baseline;"> <h2>Grafikset-Manager</h2> <div id="lss-gsa-status"></div> </div> <div id="lss-gsa-filters"> <label for="lss-gsa-filter-name">Filter Wachenname:</label> <input type="text" id="lss-gsa-filter-name" placeholder="Namensteil eingeben..."> <label for="lss-gsa-filter-type">Filter Wachentyp:</label> <select id="lss-gsa-filter-type"></select> <label for="lss-gsa-filter-lst">Filter Leitstelle:</label> <select id="lss-gsa-filter-lst"></select> </div> <div> <input type="checkbox" id="lss-gsa-select-all"> <label for="lss-gsa-select-all"><strong>Alle angezeigten Wachen auswählen / abwählen</strong></label> </div> <div id="lss-gsa-building-list"></div> <div id="lss-gsa-filters" style="grid-template-columns: auto 1fr;"> <label for="lss-gsa-filter-set">Filter Grafikset:</label> <input type="text" id="lss-gsa-filter-set" placeholder="Namensteil eingeben..."> </div> <div id="lss-gsa-controls"> <select id="lss-gsa-set-select" size="5" style="width:100%"></select> </div> <div id="lss-gsa-footer"> <button id="lss-gsa-assign-btn" class="btn btn-success">Ausgewähltes Set zuweisen</button> <button id="lss-gsa-close-btn" class="btn btn-danger">Schließen</button> </div> <pre id="lss-gsa-log"></pre> </div> </div> `; document.body.insertAdjacentHTML('beforeend', modalHTML); // Event Listeners document.getElementById('lss-gsa-assign-btn').addEventListener('click', startUpdateProcess); document.getElementById('lss-gsa-close-btn').addEventListener('click', closeModal); document.getElementById('lss-gsa-modal').addEventListener('click', (e) => e.target.id === 'lss-gsa-modal' && closeModal()); document.getElementById('lss-gsa-select-all').addEventListener('change', (e) => { document.querySelectorAll('#lss-gsa-building-list input[type="checkbox"]').forEach(cb => cb.checked = e.target.checked); }); document.getElementById('lss-gsa-filter-name').addEventListener('input', populateBuildingList); document.getElementById('lss-gsa-filter-type').addEventListener('change', populateBuildingList); document.getElementById('lss-gsa-filter-lst').addEventListener('change', populateBuildingList); document.getElementById('lss-gsa-filter-set').addEventListener('input', populateGraphicSetList); } function createMenuButton() { const anchorElement = document.getElementById('graphic_packs'); if (!anchorElement) { console.error("Grafikset-Manager: Konnte den Menüpunkt 'Grafikset' nicht finden, um den Button anzuhängen."); return; } const newLi = document.createElement('li'); newLi.setAttribute('role', 'presentation'); const newA = document.createElement('a'); newA.href = "#"; // Verwenden eines anderen, passenderen Icons (Glyphicon) newA.innerHTML = `<span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> Grafikset-Manager`; newA.style.cursor = "pointer"; newLi.appendChild(newA); newLi.addEventListener('click', (e) => { e.preventDefault(); openModal(); }); anchorElement.insertAdjacentElement('afterend', newLi); } // --- Skriptstart --- setupUI(); createMenuButton(); })();