2552 lines
88 KiB
JavaScript
2552 lines
88 KiB
JavaScript
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 = `
|
|
<div class="color-box" style="${bgStyle}">
|
|
${tile.collision ? '<span class="collision-icon">🚫</span>' : ""}
|
|
</div>
|
|
<div class="tile-label">${tile.name}</div>
|
|
`;
|
|
|
|
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 = `<option value="">Map laden...</option>`;
|
|
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(); |