const canvas = document.getElementById("canvas"); const ctx = canvas.getContext("2d"); ctx.imageSmoothingEnabled = false; // ------------------------------------------------------------- // ZUGRIFFSSCHUTZ: nur eingeloggte Admins dürfen den Map-Editor nutzen // ------------------------------------------------------------- const token = localStorage.getItem("token"); // Voll-Admin ODER die Berechtigung "manage_maps" reicht - der Server prüft // das final ab, authFetch() fängt fehlende Rechte sauber ab let isAdmin = localStorage.getItem("isAdmin") === "true"; if (!token) { alert("Bitte zuerst einloggen."); location.href = "/index.html"; } async function authFetch(url, options = {}) { options.headers = { ...(options.headers || {}), "Authorization": "Bearer " + token }; const res = await fetch(url, options); if (res.status === 401 || res.status === 403) { alert("Sitzung abgelaufen oder kein Admin-Zugriff. Bitte erneut einloggen."); location.href = "/index.html"; throw new Error("Nicht autorisiert"); } return res; } const mapNameInput = document.getElementById("mapName"); const mapSelect = document.getElementById("mapSelect"); const newMapBtn = document.getElementById("newMapBtn"); const saveMapBtn = document.getElementById("saveMapBtn"); const modeSelect = document.getElementById("modeSelect"); // ------------------------------------------------------------- // Icon-Werkzeugleiste (steuert weiterhin das versteckte modeSelect, // damit der restliche Code unverändert "modeSelect.value" lesen kann) // ------------------------------------------------------------- const modeButtons = document.querySelectorAll(".mode-btn"); function setToolbarActive(modeValue) { modeButtons.forEach(btn => { btn.classList.toggle("active", btn.dataset.mode === modeValue); }); } modeButtons.forEach(btn => { btn.addEventListener("click", () => { modeSelect.value = btn.dataset.mode; setToolbarActive(btn.dataset.mode); }); }); setToolbarActive(modeSelect.value); // ------------------------------------------------------------- // Ebenen-Sichtbarkeit // ------------------------------------------------------------- const layerVisibility = { object: true, shop: true, atm: true, garage: true, jobcenter: true, gasstation: true, repairshop: true, house: true, jobpoint: true, taxistand: true, hospital: true, prison: true, zone: true, impound: true, firestation: true, drug_harvest: true, drug_process: true, drug_dealer: true, clothingshop: true, insuranceoffice: true, plateoffice: true, trailershop: true }; document.querySelectorAll(".layer-toggle").forEach(cb => { cb.addEventListener("change", () => { layerVisibility[cb.dataset.layer] = cb.checked; render(); }); }); const doorTargetMap = document.getElementById("doorTargetMap"); const doorTargetX = document.getElementById("doorTargetX"); const doorTargetY = document.getElementById("doorTargetY"); const mapWidthInput = document.getElementById("mapWidth"); const mapHeightInput = document.getElementById("mapHeight"); const resizeMapBtn = document.getElementById("resizeMapBtn"); const tilePalette = document.getElementById("tilePalette"); const objectPalette = document.getElementById("objectPalette"); // ------------------------------------------------------------- // KAMERA / SCROLLEN (NEU) - für große/"unendliche" Maps // ------------------------------------------------------------- const editorCam = { x: 0, y: 0 }; const panKeys = {}; const PAN_SPEED = 14; // Pixel pro Frame document.addEventListener("keydown", e => { if (["arrowup", "arrowdown", "arrowleft", "arrowright"].includes(e.key.toLowerCase())) { panKeys[e.key.toLowerCase()] = true; e.preventDefault(); } }); document.addEventListener("keyup", e => { panKeys[e.key.toLowerCase()] = false; }); // Mittlere Maustaste (Mausrad gedrückt halten) zum Ziehen/Scrollen let isDragging = false; let dragStart = { x: 0, y: 0 }; let camStart = { x: 0, y: 0 }; canvas.addEventListener("mousedown", e => { if (e.button === 1) { // mittlere Maustaste isDragging = true; dragStart = { x: e.clientX, y: e.clientY }; camStart = { x: editorCam.x, y: editorCam.y }; e.preventDefault(); } }); document.addEventListener("mousemove", e => { if (!isDragging) return; editorCam.x = clampCamX(camStart.x - (e.clientX - dragStart.x)); editorCam.y = clampCamY(camStart.y - (e.clientY - dragStart.y)); render(); }); document.addEventListener("mouseup", e => { if (e.button === 1) isDragging = false; }); canvas.addEventListener("auxclick", e => { if (e.button === 1) e.preventDefault(); // verhindert Auto-Scroll-Icon bei Mittelklick }); function clampCamX(x) { const maxX = Math.max(0, cols * tileSize - canvas.width); return Math.min(Math.max(0, x), maxX); } function clampCamY(y) { const maxY = Math.max(0, rows * tileSize - canvas.height); return Math.min(Math.max(0, y), maxY); } function panLoop() { let moved = false; if (panKeys["arrowup"]) { editorCam.y -= PAN_SPEED; moved = true; } if (panKeys["arrowdown"]) { editorCam.y += PAN_SPEED; moved = true; } if (panKeys["arrowleft"]) { editorCam.x -= PAN_SPEED; moved = true; } if (panKeys["arrowright"]) { editorCam.x += PAN_SPEED; moved = true; } if (moved) { editorCam.x = clampCamX(editorCam.x); editorCam.y = clampCamY(editorCam.y); render(); updateCamLabel(); } requestAnimationFrame(panLoop); } function updateCamLabel() { const label = document.getElementById("camLabel"); if (label) { label.textContent = `Kamera-Tile: (${Math.floor(editorCam.x / tileSize)}, ${Math.floor(editorCam.y / tileSize)})`; } } function goToTile(tx, ty) { editorCam.x = clampCamX(tx * tileSize - canvas.width / 2); editorCam.y = clampCamY(ty * tileSize - canvas.height / 2); render(); updateCamLabel(); } let objectConfig = {}; let selectedObjectType = null; let objects = []; let tileConfig = {}; let selectedTile = 1; let brushSize = 1; const brushSizeSelect = document.getElementById("brushSize"); brushSizeSelect.addEventListener("change", () => { brushSize = parseInt(brushSizeSelect.value); }); function paintTileBrush(centerX, centerY, tileId) { const half = Math.floor(brushSize / 2); for (let dy = 0; dy < brushSize; dy++) { for (let dx = 0; dx < brushSize; dx++) { const x = centerX - half + dx; const y = centerY - half + dy; if (x < 0 || y < 0 || y >= tiles.length || x >= tiles[0].length) continue; tiles[y][x] = tileId; } } } let tileSize = 32; let tiles = []; let tileRot = []; // Drehwinkel je Kachel: 0, 90, 180, 270 let doors = []; let spawn = { x: 32, y: 32 }; let cols = 20; let rows = 20; let shops = []; // kommt jetzt aus der DB (admin API), nicht mehr aus der Map-Datei let atms = []; let garages = []; // kommt ebenfalls aus der DB let jobcenters = []; // kommt ebenfalls aus der DB let gasStations = []; // kommt ebenfalls aus der DB let repairShops = []; // kommt ebenfalls aus der DB let houses = []; // kommt ebenfalls aus der DB let jobPoints = []; // kommt ebenfalls aus der DB let taxiStands = []; // kommt ebenfalls aus der DB let hospitals = []; // kommt ebenfalls aus der DB let prisons = []; // kommt ebenfalls aus der DB let impoundLots = []; // kommt ebenfalls aus der DB let fireStations = []; // kommt ebenfalls aus der DB let harvestSpots = []; // kommt ebenfalls aus der DB let processSpots = []; // kommt ebenfalls aus der DB let dealerSpots = []; // kommt ebenfalls aus der DB (Pool!) let clothingShops = []; // kommt ebenfalls aus der DB let insuranceOffices = []; // kommt ebenfalls aus der DB let plateOffices = []; // kommt ebenfalls aus der DB let trailerShops = []; // kommt ebenfalls aus der DB let allDrugTypesForEditor = []; fetch("/api/admin/drug_types", { headers: { "Authorization": "Bearer " + localStorage.getItem("token") } }) .then(res => res.json()) .then(data => { allDrugTypesForEditor = data.drugTypes || []; }); function pickDrugId() { if (allDrugTypesForEditor.length === 0) { alert("Keine Drogensorten angelegt. Erst in admin_drugs.html eine Sorte anlegen."); return null; } const list = allDrugTypesForEditor.map(d => `${d.id} (${d.name})`).join("\n"); const id = prompt(`Welche Drogensorte?\n${list}`); if (!id) return null; const match = allDrugTypesForEditor.find(d => d.id === id.trim()); if (!match) { alert("Unbekannte Sorten-ID."); return null; } return match.id; } let territoryZones = []; // kommt ebenfalls aus der DB let allJobsForEditor = []; // für Job-Auswahl bei Job-Garagen/Job-Punkten // ------------------------------------------------------------- // Tiles laden // ------------------------------------------------------------- const tileImageCache = {}; fetch("/api/tile_config") .then(res => res.json()) .then(data => { tileConfig = data; Object.entries(tileConfig).forEach(([id, tile]) => { if (tile.image) { const img = new Image(); img.src = tile.image; tileImageCache[id] = img; } }); buildTilePalette(); loadMapList(); newMap(); }); fetch("/api/object_config") .then(res => res.json()) .then(data => { objectConfig = data; buildObjectPalette(); }); // ------------------------------------------------------------- // Tile-Palette bauen // ------------------------------------------------------------- function buildTilePalette() { tilePalette.innerHTML = ""; Object.entries(tileConfig).forEach(([id, tile]) => { const swatch = document.createElement("div"); swatch.className = "tile-swatch" + (selectedTile === parseInt(id) ? " selected" : ""); swatch.dataset.id = id; swatch.title = `#${id}: ${tile.name}${tile.collision ? " (blockiert)" : ""}`; const bgStyle = tile.image ? `background-image:url('${tile.image}'); background-size:cover; image-rendering:pixelated;` : `background:${tile.color};`; swatch.innerHTML = `
${tile.collision ? '🚫' : ""}
${tile.name}
`; swatch.onclick = () => { selectedTile = parseInt(id); tilePalette.querySelectorAll(".tile-swatch").forEach(el => el.classList.remove("selected")); swatch.classList.add("selected"); }; tilePalette.appendChild(swatch); }); } // ------------------------------------------------------------- // Object-Palette bauen // ------------------------------------------------------------- function buildObjectPalette() { objectPalette.innerHTML = ""; Object.entries(objectConfig).forEach(([type, obj]) => { const div = document.createElement("div"); div.style.width = "32px"; div.style.height = "32px"; div.style.background = obj.color; div.style.border = selectedObjectType === type ? "3px solid #f5d90a" : "2px solid #000"; div.style.boxShadow = selectedObjectType === type ? "0 0 6px #f5d90a" : "none"; div.style.display = "inline-block"; div.style.margin = "4px"; div.style.cursor = "pointer"; div.title = `${type}: ${obj.name}`; div.dataset.objType = type; div.onclick = () => { selectedObjectType = type; objectPalette.querySelectorAll("div").forEach(el => { el.style.border = "2px solid #000"; el.style.boxShadow = "none"; }); div.style.border = "3px solid #f5d90a"; div.style.boxShadow = "0 0 6px #f5d90a"; }; objectPalette.appendChild(div); }); // Falls noch nichts ausgewählt ist, direkt den ersten Typ vorauswählen if (!selectedObjectType) { const first = objectPalette.querySelector("div"); if (first) first.click(); } } // ------------------------------------------------------------- // Neue Map // ------------------------------------------------------------- function newMap() { tiles = []; tileRot = []; for (let y = 0; y < rows; y++) { tiles[y] = []; tileRot[y] = []; for (let x = 0; x < cols; x++) { tiles[y][x] = 1; tileRot[y][x] = 0; } } doors = []; objects = []; atms = []; shops = []; // wird erst befüllt, sobald die Map einen gespeicherten Namen hat spawn = { x: 32, y: 32 }; editorCam.x = 0; editorCam.y = 0; updateCamLabel(); render(); } newMapBtn.onclick = () => { if (!confirm("Neue, leere Map erstellen? Nicht gespeicherte Änderungen an der aktuellen Map gehen verloren.")) { return; } const name = prompt("Name für die neue Map (z.B. wald):"); if (!name) return; mapNameInput.value = name.trim(); mapSelect.value = ""; newMap(); alert(`Leere Map "${name.trim()}" ist bereit zum Bearbeiten. Nicht vergessen: oben auf "Speichern" klicken, damit sie wirklich angelegt wird!`); }; // ------------------------------------------------------------- // Map-Liste laden // ------------------------------------------------------------- function loadMapList() { fetch("/api/get_maps") .then(res => res.json()) .then(data => { mapSelect.innerHTML = ``; Object.keys(data.maps).forEach(name => { const opt = document.createElement("option"); opt.value = name; opt.textContent = name; mapSelect.appendChild(opt); }); }); } // ------------------------------------------------------------- // Shops für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadShopsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { shops = []; render(); return; } const res = await authFetch("/api/admin/shops"); const data = await res.json(); shops = (data.shops || []).filter(s => s.world === world); render(); } // ------------------------------------------------------------- // Garagen für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadGaragesForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { garages = []; render(); return; } const res = await authFetch("/api/admin/garages"); const data = await res.json(); garages = (data.garages || []).filter(g => g.world === world); render(); } // ------------------------------------------------------------- // Jobcenter für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadJobcentersForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { jobcenters = []; render(); return; } const res = await authFetch("/api/admin/jobcenters"); const data = await res.json(); jobcenters = (data.jobcenters || []).filter(j => j.world === world); render(); } // ------------------------------------------------------------- // Tankstellen für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadGasStationsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { gasStations = []; render(); return; } const res = await authFetch("/api/admin/gas_stations"); const data = await res.json(); gasStations = (data.gasStations || []).filter(g => g.world === world); render(); } // ------------------------------------------------------------- // Werkstätten für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadRepairShopsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { repairShops = []; render(); return; } const res = await authFetch("/api/admin/repair_shops"); const data = await res.json(); repairShops = (data.repairShops || []).filter(r => r.world === world); render(); } // ------------------------------------------------------------- // Häuser für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadHousesForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { houses = []; render(); return; } const res = await authFetch("/api/admin/houses"); const data = await res.json(); houses = (data.houses || []).filter(h => h.world === world); render(); } // ------------------------------------------------------------- // Job-Punkte für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadJobPointsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { jobPoints = []; render(); return; } const res = await authFetch("/api/admin/job_points"); const data = await res.json(); jobPoints = (data.jobPoints || []).filter(jp => jp.world === world); render(); } // ------------------------------------------------------------- // Taxi-Stände für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadTaxiStandsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { taxiStands = []; render(); return; } const res = await authFetch("/api/admin/taxi_stands"); const data = await res.json(); taxiStands = (data.taxiStands || []).filter(t => t.world === world); render(); } // ------------------------------------------------------------- // Krankenhäuser für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadHospitalsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { hospitals = []; render(); return; } const res = await authFetch("/api/admin/hospitals"); const data = await res.json(); hospitals = (data.hospitals || []).filter(h => h.world === world); render(); } // ------------------------------------------------------------- // Gefängnisse für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadPrisonsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { prisons = []; render(); return; } const res = await authFetch("/api/admin/prisons"); const data = await res.json(); prisons = (data.prisons || []).filter(p => p.world === world); render(); } // ------------------------------------------------------------- // Abschlepphöfe für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadImpoundLotsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { impoundLots = []; render(); return; } const res = await authFetch("/api/admin/impound_lots"); const data = await res.json(); impoundLots = (data.lots || []).filter(l => l.world === world); render(); } // ------------------------------------------------------------- // Feuerwachen für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadFireStationsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { fireStations = []; render(); return; } const res = await authFetch("/api/admin/fire_stations"); const data = await res.json(); fireStations = (data.stations || []).filter(s => s.world === world); render(); } // ------------------------------------------------------------- // Drogen-Standorte für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadHarvestSpotsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { harvestSpots = []; render(); return; } const res = await authFetch("/api/admin/drug_harvest_spots"); const data = await res.json(); harvestSpots = (data.spots || []).filter(s => s.world === world); render(); } async function loadProcessSpotsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { processSpots = []; render(); return; } const res = await authFetch("/api/admin/drug_process_spots"); const data = await res.json(); processSpots = (data.spots || []).filter(s => s.world === world); render(); } async function loadDealerSpotsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { dealerSpots = []; render(); return; } const res = await authFetch("/api/admin/drug_dealer_spots"); const data = await res.json(); dealerSpots = (data.spots || []).filter(s => s.world === world); render(); } async function loadClothingShopsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { clothingShops = []; render(); return; } const res = await authFetch("/api/admin/clothing_shops"); const data = await res.json(); clothingShops = (data.shops || []).filter(s => s.world === world); render(); } async function loadInsuranceOfficesForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { insuranceOffices = []; render(); return; } const res = await authFetch("/api/admin/insurance_offices"); const data = await res.json(); insuranceOffices = (data.offices || []).filter(s => s.world === world); render(); } async function loadPlateOfficesForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { plateOffices = []; render(); return; } const res = await authFetch("/api/admin/plate_offices"); const data = await res.json(); plateOffices = (data.offices || []).filter(s => s.world === world); render(); } async function loadTrailerShopsForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { trailerShops = []; render(); return; } const res = await authFetch("/api/admin/trailer_shops"); const data = await res.json(); trailerShops = (data.shops || []).filter(s => s.world === world); render(); } // ------------------------------------------------------------- // Territoriums-Zonen für die aktuelle Map aus der DB laden (NEU) // ------------------------------------------------------------- async function loadTerritoryZonesForCurrentMap() { const world = mapNameInput.value.trim(); if (!world) { territoryZones = []; render(); return; } const res = await authFetch("/api/admin/territory_zones"); const data = await res.json(); territoryZones = (data.zones || []).filter(z => z.world === world); render(); } // Jobliste laden (für Auswahl bei Job-Garage/Job-Punkt platzieren) async function loadJobsForEditor() { const res = await authFetch("/api/admin/jobs"); const data = await res.json(); allJobsForEditor = data.jobs || []; } loadJobsForEditor(); function pickJobId() { if (allJobsForEditor.length === 0) { alert("Keine Jobs angelegt. Erst in der Job-Verwaltung einen Job anlegen."); return null; } const list = allJobsForEditor.map(j => `${j.id} = ${j.name}`).join("\n"); const idStr = prompt(`Welcher Job? (ID eingeben)\n\n${list}`); if (!idStr) return null; const id = Number(idStr); if (!allJobsForEditor.some(j => j.id === id)) { alert("Ungültige Job-ID."); return null; } return id; } // ------------------------------------------------------------- // Map laden // ------------------------------------------------------------- mapSelect.onchange = () => { const name = mapSelect.value; if (!name) return; fetch("/api/get_maps") .then(res => res.json()) .then(data => { const map = data.maps[name]; mapNameInput.value = map.name; tiles = map.tiles; doors = map.doors || []; objects = map.objects || []; atms = map.atms || []; spawn = map.spawn; rows = tiles.length; cols = tiles[0].length; // tileRot: falls die Map noch keine Drehwinkel-Daten hat (ältere Karte), mit 0 auffüllen if (map.tileRot && map.tileRot.length === rows) { tileRot = map.tileRot; } else { tileRot = []; for (let y = 0; y < rows; y++) { tileRot[y] = []; for (let x = 0; x < cols; x++) tileRot[y][x] = 0; } } editorCam.x = 0; editorCam.y = 0; updateCamLabel(); render(); loadShopsForCurrentMap(); // Shops kommen aus der DB, nicht aus der Map-Datei loadGaragesForCurrentMap(); // Garagen ebenfalls aus der DB loadJobcentersForCurrentMap(); // Jobcenter ebenfalls aus der DB loadGasStationsForCurrentMap(); // Tankstellen ebenfalls aus der DB loadRepairShopsForCurrentMap(); // Werkstätten ebenfalls aus der DB loadHousesForCurrentMap(); loadJobPointsForCurrentMap(); loadTaxiStandsForCurrentMap(); loadHospitalsForCurrentMap(); loadPrisonsForCurrentMap(); loadImpoundLotsForCurrentMap(); loadTerritoryZonesForCurrentMap(); loadFireStationsForCurrentMap(); loadHarvestSpotsForCurrentMap(); loadProcessSpotsForCurrentMap(); loadDealerSpotsForCurrentMap(); loadClothingShopsForCurrentMap(); loadInsuranceOfficesForCurrentMap(); loadPlateOfficesForCurrentMap(); loadTrailerShopsForCurrentMap(); }); }; // ------------------------------------------------------------- // Map speichern // ------------------------------------------------------------- function buildMapData() { const name = mapNameInput.value.trim(); return { name, spawn, tiles, tileRot, doors, objects, atms // shops NICHT mehr mitspeichern - die leben jetzt in der DB }; } saveMapBtn.onclick = () => { const name = mapNameInput.value.trim(); if (!name) return alert("Map-Name fehlt!"); authFetch("/api/save_map", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, data: buildMapData() }) }) .then(res => res.json()) .then(data => { if (data.ok) { alert("Map gespeichert!"); loadMapList(); loadShopsForCurrentMap(); loadGaragesForCurrentMap(); loadJobcentersForCurrentMap(); loadGasStationsForCurrentMap(); loadRepairShopsForCurrentMap(); loadHousesForCurrentMap(); loadJobPointsForCurrentMap(); loadTaxiStandsForCurrentMap(); loadHospitalsForCurrentMap(); loadPrisonsForCurrentMap(); loadTerritoryZonesForCurrentMap(); loadImpoundLotsForCurrentMap(); loadFireStationsForCurrentMap(); loadHarvestSpotsForCurrentMap(); loadProcessSpotsForCurrentMap(); loadDealerSpotsForCurrentMap(); loadClothingShopsForCurrentMap(); loadInsuranceOfficesForCurrentMap(); loadPlateOfficesForCurrentMap(); loadTrailerShopsForCurrentMap(); } else { alert("Fehler: " + data.error); } }); }; function saveMap() { const name = mapNameInput.value.trim(); if (!name) return alert("Map-Name fehlt!"); authFetch("/api/save_map", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, data: buildMapData() }) }) .then(res => res.json()) .then(data => { if (data.ok) { alert("Map gespeichert!"); loadMapList(); } else { alert("Fehler: " + data.error); } }); } // ------------------------------------------------------------- // Map-Größe ändern // ------------------------------------------------------------- resizeMapBtn.onclick = () => { const newW = parseInt(mapWidthInput.value); const newH = parseInt(mapHeightInput.value); if (!newW || !newH) return; const newTiles = []; const newTileRot = []; for (let y = 0; y < newH; y++) { newTiles[y] = []; newTileRot[y] = []; for (let x = 0; x < newW; x++) { newTiles[y][x] = tiles[y]?.[x] ?? 1; newTileRot[y][x] = tileRot[y]?.[x] ?? 0; } } tiles = newTiles; tileRot = newTileRot; cols = newW; rows = newH; render(); }; // ------------------------------------------------------------- // Canvas Click // ------------------------------------------------------------- canvas.addEventListener("contextmenu", e => e.preventDefault()); let isPaintDragging = false; let paintDragButton = 0; canvas.addEventListener("mousedown", e => { if (e.button === 1) return; // Mittelklick wird vom Dragging-Handler oben behandelt e.preventDefault(); if (modeSelect.value === "tile") { isPaintDragging = true; paintDragButton = e.button; } handleCanvasClick(e); }); canvas.addEventListener("mousemove", e => { if (!isPaintDragging || modeSelect.value !== "tile") return; const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left + editorCam.x; const my = e.clientY - rect.top + editorCam.y; const tileX = Math.floor(mx / tileSize); const tileY = Math.floor(my / tileSize); if (tileX < 0 || tileY < 0 || tileX >= cols || tileY >= rows) return; paintTileBrush(tileX, tileY, paintDragButton === 0 ? selectedTile : 0); render(); }); document.addEventListener("mouseup", () => { isPaintDragging = false; }); function handleCanvasClick(e) { const rect = canvas.getBoundingClientRect(); const mx = e.clientX - rect.left + editorCam.x; const my = e.clientY - rect.top + editorCam.y; const tileX = Math.floor(mx / tileSize); const tileY = Math.floor(my / tileSize); if (tileX < 0 || tileY < 0 || tileX >= cols || tileY >= rows) return; const mode = modeSelect.value; // TILES if (mode === "tile") { paintTileBrush(tileX, tileY, e.button === 0 ? selectedTile : 0); render(); return; } // DREHEN: zuerst prüfen, ob an dieser Stelle ein Objekt steht (das hat Vorrang), // sonst wird stattdessen die Tile darunter gedreht if (mode === "rotate") { const ox = tileX * tileSize; const oy = tileY * tileSize; const obj = objects.find(o => o.x === ox && o.y === oy); if (obj) { const current = obj.rot || 0; if (e.button === 0) obj.rot = (current + 90) % 360; else if (e.button === 2) obj.rot = (current + 270) % 360; render(); return; } if (tileY < 0 || tileX < 0 || tileY >= tiles.length || tileX >= tiles[0].length) return; if (!tileRot[tileY]) tileRot[tileY] = []; const current = tileRot[tileY][tileX] || 0; if (e.button === 0) { tileRot[tileY][tileX] = (current + 90) % 360; } else if (e.button === 2) { tileRot[tileY][tileX] = (current + 270) % 360; } render(); return; } // SPAWN if (mode === "spawn") { if (e.button === 0) { spawn.x = tileX * tileSize; spawn.y = tileY * tileSize; render(); } return; } // DOORS if (mode === "door") { if (e.button === 0) { doors.push({ x: tileX * tileSize, y: tileY * tileSize, targetMap: doorTargetMap.value, targetX: parseInt(doorTargetX.value), targetY: parseInt(doorTargetY.value) }); render(); } if (e.button === 2) { doors = doors.filter(d => !(d.x === tileX * tileSize && d.y === tileY * tileSize)); render(); } return; } // OBJECTS if (mode === "object") { const ox = tileX * tileSize; const oy = tileY * tileSize; if (e.button === 0) { if (!selectedObjectType) { alert("Bitte zuerst einen Objekt-Typ in der Palette unten auswählen."); return; } const newObj = { type: selectedObjectType, x: ox, y: oy }; const cfg = objectConfig[selectedObjectType]; if (cfg && cfg.action === "toggle_gate") { const side = prompt( "Einbahn-Sperre für dieses Tor? Leer lassen = von beiden Seiten bedienbar.\n" + "Gültige Werte: top, bottom, left, right\n" + "(die gesperrte Seite kann das Tor NICHT öffnen/schließen)", "" ); const trimmed = (side || "").trim().toLowerCase(); if (["top", "bottom", "left", "right"].includes(trimmed)) { newObj.oneWaySide = trimmed; } } objects.push(newObj); render(); } if (e.button === 2) { objects = objects.filter(o => !(o.x === ox && o.y === oy)); render(); } return; } // SHOPS (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "shop") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = shops.find(s => s.x === tileX && s.y === tileY); if (existing) { alert(`Hier steht schon Shop "#${existing.id}". Items verwaltest du im Admin-Panel.`); return; } const name = prompt("Shop-Name:"); if (!name) return; authFetch("/api/admin/shops", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadShopsForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = shops.find(s => s.x === tileX && s.y === tileY); if (!existing) return; if (!confirm(`Shop "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/shops/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadShopsForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // GARAGEN (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "garage") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = garages.find(g => g.x === tileX && g.y === tileY); if (existing) return; const name = prompt("Garage-Name:"); if (!name) return; const isJobGarage = confirm("Soll das eine JOB-Garage (Fuhrpark) sein?\nOK = Job-Garage, Abbrechen = normale private Garage"); let jobId = null; if (isJobGarage) { jobId = pickJobId(); if (jobId === null) return; } authFetch("/api/admin/garages", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY, jobId }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadGaragesForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = garages.find(g => g.x === tileX && g.y === tileY); if (!existing) return; if (!confirm(`Garage "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/garages/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadGaragesForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // HÄUSER (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "house") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = houses.find(h => h.x === tileX && h.y === tileY); if (existing) return; const name = prompt("Haus-Name:"); if (!name) return; const priceStr = prompt("Kaufpreis ($):", "1000"); const price = Number(priceStr); if (isNaN(price) || price <= 0) return; authFetch("/api/admin/houses", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY, price }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadHousesForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = houses.find(h => h.x === tileX && h.y === tileY); if (!existing) return; if (!confirm(`Haus "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/houses/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadHousesForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // JOB-PUNKTE (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "jobpoint") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = jobPoints.find(jp => jp.x === tileX && jp.y === tileY); if (existing) return; const jobId = pickJobId(); if (jobId === null) return; const name = prompt("Name des Zielpunkts (z.B. 'Ladung abholen'):"); if (!name) return; const itemId = prompt( "Item-ID, das hier abgeholt/abgeliefert wird (muss in der Item-Verwaltung existieren):", "package" ); if (!itemId) return; const isDropoff = confirm("Ist das ein LIEFERPUNKT (Item abgeben)?\nOK = Lieferpunkt, Abbrechen = Abholpunkt"); const kind = isDropoff ? "dropoff" : "pickup"; let reward = 50; if (isDropoff) { const rewardStr = prompt("Belohnung pro Lieferung ($):", "50"); reward = Number(rewardStr) || 50; } authFetch("/api/admin/job_points", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jobId, name, world, x: tileX, y: tileY, kind, reward, itemId }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadJobPointsForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = jobPoints.find(jp => jp.x === tileX && jp.y === tileY); if (!existing) return; if (!confirm(`Job-Punkt "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/job_points/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadJobPointsForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // TERRITORIUMS-ZONEN (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "zone") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = territoryZones.find(z => z.x === tileX && z.y === tileY); if (existing) return; const name = prompt("Name der Zone (z.B. 'Hafenviertel'):"); if (!name) return; authFetch("/api/admin/territory_zones", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadTerritoryZonesForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = territoryZones.find(z => z.x === tileX && z.y === tileY); if (!existing) return; if (!confirm(`Zone "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/territory_zones/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadTerritoryZonesForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // DROGEN-ANBAUSTELLEN (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "drug_harvest") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = harvestSpots.find(s => s.x === tileX && s.y === tileY); if (existing) return; const drugId = pickDrugId(); if (!drugId) return; const name = prompt("Name der Anbaustelle:", "Anbaustelle"); if (!name) return; authFetch("/api/admin/drug_harvest_spots", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ drugId, name, world, x: tileX, y: tileY }) }).then(res => res.json()).then(data => { if (data.ok) loadHarvestSpotsForCurrentMap(); else alert("Fehler: " + (data.error || "unbekannt")); }); } if (e.button === 2) { const existing = harvestSpots.find(s => s.x === tileX && s.y === tileY); if (!existing) return; if (!confirm(`Anbaustelle "#${existing.id}" wirklich löschen?`)) return; authFetch("/api/admin/drug_harvest_spots/" + existing.id, { method: "DELETE" }) .then(res => res.json()).then(data => { if (data.ok) loadHarvestSpotsForCurrentMap(); else alert("Löschen fehlgeschlagen."); }); } return; } // DROGEN-LABORE (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "drug_process") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = processSpots.find(s => s.x === tileX && s.y === tileY); if (existing) return; const drugId = pickDrugId(); if (!drugId) return; const name = prompt("Name des Labors:", "Labor"); if (!name) return; authFetch("/api/admin/drug_process_spots", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ drugId, name, world, x: tileX, y: tileY }) }).then(res => res.json()).then(data => { if (data.ok) loadProcessSpotsForCurrentMap(); else alert("Fehler: " + (data.error || "unbekannt")); }); } if (e.button === 2) { const existing = processSpots.find(s => s.x === tileX && s.y === tileY); if (!existing) return; if (!confirm(`Labor "#${existing.id}" wirklich löschen?`)) return; authFetch("/api/admin/drug_process_spots/" + existing.id, { method: "DELETE" }) .then(res => res.json()).then(data => { if (data.ok) loadProcessSpotsForCurrentMap(); else alert("Löschen fehlgeschlagen."); }); } return; } // DROGEN-VERKAUFSORTE-POOL (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "drug_dealer") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = dealerSpots.find(s => s.x === tileX && s.y === tileY); if (existing) return; const drugId = pickDrugId(); if (!drugId) return; const name = prompt("Name des Verkaufsorts:", "Verkaufsstelle"); if (!name) return; authFetch("/api/admin/drug_dealer_spots", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ drugId, name, world, x: tileX, y: tileY }) }).then(res => res.json()).then(data => { if (data.ok) loadDealerSpotsForCurrentMap(); else alert("Fehler: " + (data.error || "unbekannt")); }); } if (e.button === 2) { const existing = dealerSpots.find(s => s.x === tileX && s.y === tileY); if (!existing) return; if (!confirm(`Verkaufsort "#${existing.id}" wirklich aus dem Pool löschen?`)) return; authFetch("/api/admin/drug_dealer_spots/" + existing.id, { method: "DELETE" }) .then(res => res.json()).then(data => { if (data.ok) loadDealerSpotsForCurrentMap(); else alert("Löschen fehlgeschlagen."); }); } return; } // KLEIDUNGSLÄDEN (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "trailershop") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = trailerShops.find(s => s.x === tileX && s.y === tileY); if (existing) return; const name = prompt("Name der Anhänger-Shop:", "Anhänger-Shop"); if (!name) return; authFetch("/api/admin/trailer_shops", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }).then(res => res.json()).then(data => { if (data.ok) loadTrailerShopsForCurrentMap(); else alert("Fehler: " + (data.error || "unbekannt")); }); } if (e.button === 2) { const existing = trailerShops.find(s => s.x === tileX && s.y === tileY); if (!existing) return; if (!confirm(`Anhänger-Shop "#${existing.id}" wirklich löschen?`)) return; authFetch("/api/admin/trailer_shops/" + existing.id, { method: "DELETE" }) .then(res => res.json()).then(data => { if (data.ok) loadTrailerShopsForCurrentMap(); else alert("Löschen fehlgeschlagen."); }); } return; } if (mode === "insuranceoffice") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = insuranceOffices.find(s => s.x === tileX && s.y === tileY); if (existing) return; const name = prompt("Name der Versicherung:", "Versicherung"); if (!name) return; authFetch("/api/admin/insurance_offices", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }).then(res => res.json()).then(data => { if (data.ok) loadInsuranceOfficesForCurrentMap(); else alert("Fehler: " + (data.error || "unbekannt")); }); } if (e.button === 2) { const existing = insuranceOffices.find(s => s.x === tileX && s.y === tileY); if (!existing) return; if (!confirm(`Versicherung "#${existing.id}" wirklich löschen?`)) return; authFetch("/api/admin/insurance_offices/" + existing.id, { method: "DELETE" }) .then(res => res.json()).then(data => { if (data.ok) loadInsuranceOfficesForCurrentMap(); else alert("Löschen fehlgeschlagen."); }); } return; } if (mode === "plateoffice") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = plateOffices.find(s => s.x === tileX && s.y === tileY); if (existing) return; const name = prompt("Name der Zulassungsstelle:", "Kfz-Zulassungsstelle"); if (!name) return; authFetch("/api/admin/plate_offices", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }).then(res => res.json()).then(data => { if (data.ok) loadPlateOfficesForCurrentMap(); else alert("Fehler: " + (data.error || "unbekannt")); }); } if (e.button === 2) { const existing = plateOffices.find(s => s.x === tileX && s.y === tileY); if (!existing) return; if (!confirm(`Zulassungsstelle "#${existing.id}" wirklich löschen?`)) return; authFetch("/api/admin/plate_offices/" + existing.id, { method: "DELETE" }) .then(res => res.json()).then(data => { if (data.ok) loadPlateOfficesForCurrentMap(); else alert("Löschen fehlgeschlagen."); }); } return; } if (mode === "clothingshop") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = clothingShops.find(s => s.x === tileX && s.y === tileY); if (existing) return; const name = prompt("Name des Kleidungsladens:", "Kleidungsladen"); if (!name) return; authFetch("/api/admin/clothing_shops", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }).then(res => res.json()).then(data => { if (data.ok) loadClothingShopsForCurrentMap(); else alert("Fehler: " + (data.error || "unbekannt")); }); } if (e.button === 2) { const existing = clothingShops.find(s => s.x === tileX && s.y === tileY); if (!existing) return; if (!confirm(`Kleidungsladen "#${existing.id}" wirklich löschen?`)) return; authFetch("/api/admin/clothing_shops/" + existing.id, { method: "DELETE" }) .then(res => res.json()).then(data => { if (data.ok) loadClothingShopsForCurrentMap(); else alert("Löschen fehlgeschlagen."); }); } return; } // FEUERWACHEN (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "firestation") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = fireStations.find(s => s.x === tileX && s.y === tileY); if (existing) return; const name = prompt("Feuerwachen-Name:"); if (!name) return; authFetch("/api/admin/fire_stations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadFireStationsForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = fireStations.find(s => s.x === tileX && s.y === tileY); if (!existing) return; if (!confirm(`Feuerwache "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/fire_stations/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadFireStationsForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // ABSCHLEPPHÖFE (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "impound") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = impoundLots.find(l => l.x === tileX && l.y === tileY); if (existing) return; const name = prompt("Abschlepphof-Name:"); if (!name) return; authFetch("/api/admin/impound_lots", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadImpoundLotsForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = impoundLots.find(l => l.x === tileX && l.y === tileY); if (!existing) return; if (!confirm(`Abschlepphof "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/impound_lots/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadImpoundLotsForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // GEFÄNGNISSE (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "prison") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = prisons.find(p => p.x === tileX && p.y === tileY); if (existing) return; const name = prompt("Gefängnis-Name:"); if (!name) return; authFetch("/api/admin/prisons", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadPrisonsForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = prisons.find(p => p.x === tileX && p.y === tileY); if (!existing) return; if (!confirm(`Gefängnis "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/prisons/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadPrisonsForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // KRANKENHÄUSER (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "hospital") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = hospitals.find(h => h.x === tileX && h.y === tileY); if (existing) return; const name = prompt("Krankenhaus-Name:"); if (!name) return; authFetch("/api/admin/hospitals", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadHospitalsForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = hospitals.find(h => h.x === tileX && h.y === tileY); if (!existing) return; if (!confirm(`Krankenhaus "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/hospitals/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadHospitalsForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // TAXI-STÄNDE (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "taxistand") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = taxiStands.find(t => t.x === tileX && t.y === tileY); if (existing) return; const name = prompt("Taxi-Stand-Name:"); if (!name) return; authFetch("/api/admin/taxi_stands", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadTaxiStandsForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = taxiStands.find(t => t.x === tileX && t.y === tileY); if (!existing) return; if (!confirm(`Taxi-Stand "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/taxi_stands/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadTaxiStandsForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // JOBCENTER (DB-basiert: Linksklick = anlegen, Rechtsklick = löschen) if (mode === "jobcenter") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = jobcenters.find(j => j.x === tileX && j.y === tileY); if (existing) return; const name = prompt("Jobcenter-Name:"); if (!name) return; authFetch("/api/admin/jobcenters", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadJobcentersForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = jobcenters.find(j => j.x === tileX && j.y === tileY); if (!existing) return; if (!confirm(`Jobcenter "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/jobcenters/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadJobcentersForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // TANKSTELLEN (DB-basiert: Linksklick = anlegen/Preis ändern, Rechtsklick = löschen) if (mode === "gasstation") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = gasStations.find(g => g.x === tileX && g.y === tileY); if (existing) { const priceStr = prompt("Neuer Preis pro Liter ($):", existing.price); if (priceStr === null) return; const price = Number(priceStr); if (isNaN(price) || price <= 0) return; authFetch("/api/admin/gas_stations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: existing.id, price }) }) .then(res => res.json()) .then(data => { if (data.ok) loadGasStationsForCurrentMap(); else alert("Fehler beim Aktualisieren."); }); return; } const name = prompt("Tankstellen-Name:"); if (!name) return; const priceStr = prompt("Preis pro Liter ($):", "2.00"); const price = Number(priceStr); if (isNaN(price) || price <= 0) return; authFetch("/api/admin/gas_stations", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY, price }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadGasStationsForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = gasStations.find(g => g.x === tileX && g.y === tileY); if (!existing) return; if (!confirm(`Tankstelle "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/gas_stations/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadGasStationsForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // WERKSTÄTTEN (DB-basiert: Linksklick = anlegen/Preis ändern, Rechtsklick = löschen) if (mode === "repairshop") { const world = mapNameInput.value.trim(); if (!world) { alert("Bitte zuerst einen Map-Namen eingeben und die Map speichern."); return; } if (e.button === 0) { const existing = repairShops.find(r => r.x === tileX && r.y === tileY); if (existing) { const priceStr = prompt("Neuer Preis pro Reparatur-Punkt ($):", existing.price_per_point); if (priceStr === null) return; const price = Number(priceStr); if (isNaN(price) || price <= 0) return; authFetch("/api/admin/repair_shops", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: existing.id, pricePerPoint: price }) }) .then(res => res.json()) .then(data => { if (data.ok) loadRepairShopsForCurrentMap(); else alert("Fehler beim Aktualisieren."); }); return; } const name = prompt("Werkstatt-Name:"); if (!name) return; const priceStr = prompt("Preis pro Reparatur-Punkt ($):", "5.00"); const price = Number(priceStr); if (isNaN(price) || price <= 0) return; authFetch("/api/admin/repair_shops", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, world, x: tileX, y: tileY, pricePerPoint: price }) }) .then(res => res.json()) .then(data => { if (data.ok) { loadRepairShopsForCurrentMap(); } else { alert("Fehler: " + (data.error || "unbekannt")); } }); } if (e.button === 2) { const existing = repairShops.find(r => r.x === tileX && r.y === tileY); if (!existing) return; if (!confirm(`Werkstatt "#${existing.id}" hier wirklich löschen?`)) return; authFetch("/api/admin/repair_shops/" + existing.id, { method: "DELETE" }) .then(res => res.json()) .then(data => { if (data.ok) { loadRepairShopsForCurrentMap(); } else { alert("Löschen fehlgeschlagen."); } }); } return; } // ATMS (weiterhin in der Map-Datei gespeichert) if (mode === "atm") { if (e.button === 0) { const existing = atms.find(a => a.x === tileX && a.y === tileY); if (existing) return; atms.push({ id: "atm_" + Date.now(), x: tileX, y: tileY }); render(); } if (e.button === 2) { atms = atms.filter(a => !(a.x === tileX && a.y === tileY)); render(); } return; } } // ------------------------------------------------------------- // Render Shops / ATMs // ------------------------------------------------------------- function renderShops() { shops.forEach(s => { const px = s.x * tileSize - editorCam.x; const py = s.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "yellow"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "black"; ctx.font = "10px Arial"; ctx.fillText("#" + s.id, px + 2, py + 12); }); } function renderATMs() { atms.forEach(a => { const px = a.x * tileSize - editorCam.x; const py = a.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "blue"; ctx.fillRect(px, py, tileSize, tileSize); }); } function renderGarages() { garages.forEach(g => { const px = g.x * tileSize - editorCam.x; const py = g.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "green"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "10px Arial"; ctx.fillText("#" + g.id, px + 2, py + 12); }); } function renderJobcenters() { jobcenters.forEach(j => { const px = j.x * tileSize - editorCam.x; const py = j.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "purple"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "10px Arial"; ctx.fillText("#" + j.id, px + 2, py + 12); }); } function renderGasStations() { gasStations.forEach(g => { const px = g.x * tileSize - editorCam.x; const py = g.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#e67e22"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText("#" + g.id, px + 2, py + 12); ctx.fillText(g.price.toFixed(2) + "$", px + 2, py + 22); }); } function renderRepairShops() { repairShops.forEach(r => { const px = r.x * tileSize - editorCam.x; const py = r.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#7f8c8d"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText("#" + r.id, px + 2, py + 12); ctx.fillText(r.price_per_point.toFixed(2) + "$", px + 2, py + 22); }); } function renderHouses() { houses.forEach(h => { const px = h.x * tileSize - editorCam.x; const py = h.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = h.owner_id ? "#8e6b4a" : "#c9a876"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "black"; ctx.font = "9px Arial"; ctx.fillText("#" + h.id, px + 2, py + 12); ctx.fillText(h.price + "$", px + 2, py + 22); }); } function renderJobPoints() { jobPoints.forEach(jp => { const px = jp.x * tileSize - editorCam.x; const py = jp.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = jp.kind === "dropoff" ? "#3498db" : "#2ecc71"; ctx.beginPath(); ctx.arc(px + tileSize / 2, py + tileSize / 2, 12, 0, Math.PI * 2); ctx.fill(); ctx.strokeStyle = "black"; ctx.stroke(); ctx.fillStyle = "black"; ctx.font = "9px Arial"; const label = jp.kind === "dropoff" ? `Job ${jp.job_id}: ${jp.name} (${jp.reward}$)` : `Job ${jp.job_id}: ${jp.name}`; ctx.fillText(label, px - 4, py + 34); }); } function renderTaxiStands() { taxiStands.forEach(t => { const px = t.x * tileSize - editorCam.x; const py = t.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#f5d90a"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "black"; ctx.font = "9px Arial"; ctx.fillText("#" + t.id + " " + t.name, px - 4, py + 44); }); } function renderHospitals() { hospitals.forEach(h => { const px = h.x * tileSize - editorCam.x; const py = h.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#e74c3c"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "black"; ctx.font = "9px Arial"; ctx.fillText("#" + h.id + " " + h.name, px - 4, py + 44); }); } function renderPrisons() { prisons.forEach(p => { const px = p.x * tileSize - editorCam.x; const py = p.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#555555"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText("#" + p.id + " " + p.name, px - 4, py + 44); }); } function renderImpoundLots() { impoundLots.forEach(l => { const px = l.x * tileSize - editorCam.x; const py = l.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#b8860b"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText("#" + l.id + " " + l.name, px - 4, py + 44); }); } function renderFireStations() { fireStations.forEach(s => { const px = s.x * tileSize - editorCam.x; const py = s.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#c0392b"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText("#" + s.id + " " + s.name, px - 4, py + 44); }); } function renderHarvestSpots() { harvestSpots.forEach(s => { const px = s.x * tileSize - editorCam.x; const py = s.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#2ecc71"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "black"; ctx.font = "9px Arial"; ctx.fillText(`#${s.id} ${s.name} (${s.drug_id})`, px - 4, py + 44); }); } function renderProcessSpots() { processSpots.forEach(s => { const px = s.x * tileSize - editorCam.x; const py = s.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#8e44ad"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText(`#${s.id} ${s.name} (${s.drug_id})`, px - 4, py + 44); }); } function renderDealerSpots() { dealerSpots.forEach(s => { const px = s.x * tileSize - editorCam.x; const py = s.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#e67e22"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "black"; ctx.font = "9px Arial"; ctx.fillText(`#${s.id} ${s.name} (${s.drug_id}) [Pool]`, px - 4, py + 44); }); } function renderClothingShops() { clothingShops.forEach(s => { const px = s.x * tileSize - editorCam.x; const py = s.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#e84393"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText(`#${s.id} ${s.name}`, px - 4, py + 44); }); } function renderInsuranceOffices() { insuranceOffices.forEach(s => { const px = s.x * tileSize - editorCam.x; const py = s.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#0984e3"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText(`#${s.id} ${s.name}`, px - 4, py + 44); }); } function renderPlateOffices() { plateOffices.forEach(s => { const px = s.x * tileSize - editorCam.x; const py = s.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#636e72"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText(`#${s.id} ${s.name}`, px - 4, py + 44); }); } function renderTrailerShops() { trailerShops.forEach(s => { const px = s.x * tileSize - editorCam.x; const py = s.y * tileSize - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillStyle = "#8e6a3d"; ctx.fillRect(px, py, tileSize, tileSize); ctx.strokeStyle = "black"; ctx.strokeRect(px, py, tileSize, tileSize); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText(`#${s.id} ${s.name}`, px - 4, py + 44); }); } function renderTerritoryZones() { territoryZones.forEach(z => { const cx = z.x * tileSize - editorCam.x + tileSize / 2; const cy = z.y * tileSize - editorCam.y + tileSize / 2; const radius = 150; if (cx < -radius || cy < -radius || cx > canvas.width + radius || cy > canvas.height + radius) return; ctx.strokeStyle = "#9b59b6"; ctx.lineWidth = 2; ctx.setLineDash([8, 6]); ctx.beginPath(); ctx.arc(cx, cy, radius, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = "#9b59b6"; ctx.beginPath(); ctx.arc(cx, cy, 6, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "white"; ctx.font = "9px Arial"; ctx.fillText("#" + z.id + " " + z.name, cx - 4, cy - radius - 4); }); } // ------------------------------------------------------------- // Render (NEU: nur sichtbarer Ausschnitt, wichtig bei großen Maps) // ------------------------------------------------------------- function render() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Nur die Tiles zeichnen, die aktuell im sichtbaren Bereich liegen const startCol = Math.max(0, Math.floor(editorCam.x / tileSize)); const endCol = Math.min(cols, startCol + Math.ceil(canvas.width / tileSize) + 1); const startRow = Math.max(0, Math.floor(editorCam.y / tileSize)); const endRow = Math.min(rows, startRow + Math.ceil(canvas.height / tileSize) + 1); for (let y = startRow; y < endRow; y++) { for (let x = startCol; x < endCol; x++) { const id = tiles[y][x]; const tile = tileConfig[id]; const px = x * tileSize - editorCam.x; const py = y * tileSize - editorCam.y; const img = tileImageCache[id]; const rot = (tileRot[y] && tileRot[y][x]) || 0; if (img && img.complete && img.naturalWidth > 0) { if (rot !== 0) { ctx.save(); ctx.translate(px + tileSize / 2, py + tileSize / 2); ctx.rotate(rot * Math.PI / 180); ctx.drawImage(img, -tileSize / 2, -tileSize / 2, tileSize, tileSize); ctx.restore(); } else { ctx.drawImage(img, px, py, tileSize, tileSize); } } else { ctx.fillStyle = tile ? tile.color : "#000"; ctx.fillRect(px, py, tileSize, tileSize); } // kleiner Pfeil zeigt die aktuelle Drehung an (nur wenn gedreht) if (rot !== 0) { ctx.save(); ctx.translate(px + tileSize / 2, py + tileSize / 2); ctx.rotate(rot * Math.PI / 180); ctx.fillStyle = "rgba(255,255,0,0.8)"; ctx.beginPath(); ctx.moveTo(0, -tileSize / 2 + 3); ctx.lineTo(-3, -tileSize / 2 + 8); ctx.lineTo(3, -tileSize / 2 + 8); ctx.closePath(); ctx.fill(); ctx.restore(); } // PvP-sichere Zone: kleines Schild-Symbol in der Ecke if (tile && tile.pvpSafe) { ctx.font = "10px Arial"; ctx.textAlign = "left"; ctx.fillText("🛡️", px + 1, py + 11); } ctx.strokeStyle = "#333"; ctx.strokeRect(px, py, tileSize, tileSize); } } // Spawn ctx.fillStyle = "yellow"; ctx.fillRect(spawn.x - editorCam.x, spawn.y - editorCam.y, tileSize, tileSize); // Doors ctx.fillStyle = "orange"; doors.forEach(d => { const px = d.x - editorCam.x; const py = d.y - editorCam.y; if (px < -tileSize || py < -tileSize || px > canvas.width || py > canvas.height) return; ctx.fillRect(px, py, tileSize, tileSize); }); // Objects if (layerVisibility.object) { objects.forEach(o => { const cfg = objectConfig[o.type]; if (!cfg) return; const px = o.x - editorCam.x; const py = o.y - (cfg.height - tileSize) - editorCam.y; if (px < -cfg.width || py < -cfg.height || px > canvas.width || py > canvas.height) return; const rot = o.rot || 0; ctx.save(); if (rot !== 0) { ctx.translate(px + cfg.width / 2, py + cfg.height / 2); ctx.rotate(rot * Math.PI / 180); ctx.translate(-cfg.width / 2, -cfg.height / 2); } else { ctx.translate(px, py); } ctx.fillStyle = cfg.color; ctx.fillRect(0, 0, cfg.width, cfg.height); ctx.strokeStyle = "#fff"; ctx.lineWidth = 1; ctx.strokeRect(0, 0, cfg.width, cfg.height); // Einbahn-Sperre: roter Balken auf der gesperrten Seite (dreht sich mit) if (o.oneWaySide) { ctx.fillStyle = "rgba(255,0,0,0.8)"; const barThickness = 4; if (o.oneWaySide === "top") ctx.fillRect(0, 0, cfg.width, barThickness); if (o.oneWaySide === "bottom") ctx.fillRect(0, cfg.height - barThickness, cfg.width, barThickness); if (o.oneWaySide === "left") ctx.fillRect(0, 0, barThickness, cfg.height); if (o.oneWaySide === "right") ctx.fillRect(cfg.width - barThickness, 0, barThickness, cfg.height); } // Kleiner Pfeil zeigt die Drehung an (nur wenn tatsächlich gedreht) if (rot !== 0) { ctx.fillStyle = "rgba(255,255,0,0.9)"; ctx.beginPath(); ctx.moveTo(cfg.width / 2, 4); ctx.lineTo(cfg.width / 2 - 4, 11); ctx.lineTo(cfg.width / 2 + 4, 11); ctx.closePath(); ctx.fill(); } ctx.restore(); }); } // Shops & ATMs & Garagen if (layerVisibility.shop) renderShops(); if (layerVisibility.atm) renderATMs(); if (layerVisibility.garage) renderGarages(); if (layerVisibility.jobcenter) renderJobcenters(); if (layerVisibility.gasstation) renderGasStations(); if (layerVisibility.repairshop) renderRepairShops(); if (layerVisibility.house) renderHouses(); if (layerVisibility.jobpoint) renderJobPoints(); if (layerVisibility.taxistand) renderTaxiStands(); if (layerVisibility.hospital) renderHospitals(); if (layerVisibility.prison) renderPrisons(); if (layerVisibility.zone) renderTerritoryZones(); if (layerVisibility.impound) renderImpoundLots(); if (layerVisibility.firestation) renderFireStations(); if (layerVisibility.drug_harvest) renderHarvestSpots(); if (layerVisibility.drug_process) renderProcessSpots(); if (layerVisibility.drug_dealer) renderDealerSpots(); if (layerVisibility.clothingshop) renderClothingShops(); if (layerVisibility.insuranceoffice) renderInsuranceOffices(); if (layerVisibility.plateoffice) renderPlateOffices(); if (layerVisibility.trailershop) renderTrailerShops(); } panLoop(); updateCamLabel();