902 lines
31 KiB
HTML
902 lines
31 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Tiles & Objekte</title>
|
||
<style>
|
||
body {
|
||
margin: 0;
|
||
background: #111;
|
||
color: #eee;
|
||
font-family: Arial, sans-serif;
|
||
padding: 20px;
|
||
}
|
||
h1 { margin-top: 0; }
|
||
a.back { color: #6fbf73; text-decoration: none; font-size: 14px; }
|
||
a.back:hover { text-decoration: underline; }
|
||
|
||
.panel {
|
||
background: #1a1a1a;
|
||
border: 1px solid #333;
|
||
border-radius: 8px;
|
||
padding: 16px;
|
||
margin-top: 16px;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin-top: 10px;
|
||
}
|
||
th, td {
|
||
text-align: left;
|
||
padding: 8px;
|
||
border-bottom: 1px solid #333;
|
||
font-size: 14px;
|
||
}
|
||
th { color: #aaa; font-weight: normal; }
|
||
|
||
input, select {
|
||
background: #222;
|
||
border: 1px solid #444;
|
||
color: #eee;
|
||
padding: 6px 8px;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
box-sizing: border-box;
|
||
}
|
||
input[type="checkbox"] { width: auto; }
|
||
|
||
button {
|
||
background: #2c7a3d;
|
||
border: none;
|
||
color: white;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
}
|
||
button.danger { background: #a83232; }
|
||
button:hover { opacity: 0.85; }
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
|
||
gap: 10px;
|
||
margin-top: 12px;
|
||
align-items: end;
|
||
}
|
||
.form-grid label {
|
||
display: block;
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-bottom: 4px;
|
||
}
|
||
.form-grid input, .form-grid select { width: 100%; }
|
||
.checkbox-row { display: flex; align-items: center; gap: 6px; }
|
||
|
||
.swatch {
|
||
display: inline-block;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 3px;
|
||
border: 1px solid #555;
|
||
vertical-align: middle;
|
||
margin-right: 6px;
|
||
}
|
||
|
||
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<a class="back" href="/index.html">← zurück zur Startseite</a>
|
||
<h1>🧱 Tiles & Objekte</h1>
|
||
|
||
<!-- TILES -->
|
||
<div class="panel">
|
||
<h2 style="margin-top:0;">Tiles</h2>
|
||
<div class="msg" id="tileMsg"></div>
|
||
|
||
<table>
|
||
<thead><tr><th></th><th>ID</th><th>Name</th><th>Kollision</th><th>PvP-sicher</th><th>Straße</th><th></th></tr></thead>
|
||
<tbody id="tilesTableBody"></tbody>
|
||
</table>
|
||
|
||
<h3>Tile hinzufügen / bearbeiten</h3>
|
||
<div class="form-grid">
|
||
<div>
|
||
<label>ID (Zahl)</label>
|
||
<input id="tileId" type="number">
|
||
</div>
|
||
<div>
|
||
<label>Name</label>
|
||
<input id="tileName" placeholder="z.B. Gras">
|
||
</div>
|
||
<div>
|
||
<label>Grundfarbe</label>
|
||
<input id="tileColor" type="color" value="#3a9d3a">
|
||
</div>
|
||
<div class="checkbox-row">
|
||
<input id="tileCollision" type="checkbox">
|
||
<label for="tileCollision" style="margin:0;">Kollision</label>
|
||
</div>
|
||
<div class="checkbox-row">
|
||
<input id="tilePvpSafe" type="checkbox">
|
||
<label for="tilePvpSafe" style="margin:0;">🛡️ PvP-sichere Zone (kein Angreifen möglich)</label>
|
||
</div>
|
||
<div class="checkbox-row">
|
||
<input id="tileIsRoad" type="checkbox">
|
||
<label for="tileIsRoad" style="margin:0;">🛣️ Straße (NPC-Verkehr fährt nur hier)</label>
|
||
</div>
|
||
<div>
|
||
<button onclick="saveTile()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
|
||
<h3>Textur malen (optional)</h3>
|
||
<p style="color:#888; font-size:12px; margin-top:0;">
|
||
Statt einer reinen Flächenfarbe kannst du hier ein kleines 16×16-Pixel-Muster malen
|
||
(z.B. eine Mittellinie für eine Straße). Ohne gemalte Textur wird einfach die Grundfarbe genutzt.
|
||
</p>
|
||
<div style="display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap;">
|
||
<div>
|
||
<canvas id="pixelCanvas" width="480" height="480" style="border:1px solid #444; cursor:crosshair; image-rendering:pixelated; background:#3a9d3a;"></canvas>
|
||
</div>
|
||
<div style="display:flex; flex-direction:column; gap:8px; min-width:180px;">
|
||
<label style="font-size:12px; color:#999;">Malfarbe</label>
|
||
<input id="pixelColor" type="color" value="#ffffff" style="width:60px; height:36px; padding:2px; cursor:pointer;">
|
||
|
||
<div style="display:flex; gap:4px; flex-wrap:wrap; margin-top:4px;">
|
||
<button onclick="presetStripeMiddle()" style="font-size:11px;">Strich Mitte</button>
|
||
<button onclick="presetBorder()" style="font-size:11px;">Rand</button>
|
||
<button onclick="presetDot()" style="font-size:11px;">Punkt Mitte</button>
|
||
</div>
|
||
|
||
<button onclick="clearPixelCanvas()" style="background:#a83232; margin-top:8px;">Löschen (Grundfarbe)</button>
|
||
<button onclick="saveTileTexture()" style="background:#2c7a3d;">Textur speichern</button>
|
||
<button onclick="removeTileTexture()" style="background:#555;">Textur entfernen</button>
|
||
<div class="msg" id="textureMsg"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- OBJECTS -->
|
||
<div class="panel">
|
||
<h2 style="margin-top:0;">Objekte</h2>
|
||
<div class="msg" id="objMsg"></div>
|
||
|
||
<table>
|
||
<thead><tr><th></th><th>Typ</th><th>Name</th><th>Größe</th><th>Kollision</th><th>Interaktiv</th><th>Aktion</th><th>Nachts</th><th></th></tr></thead>
|
||
<tbody id="objectsTableBody"></tbody>
|
||
</table>
|
||
|
||
<h3>Objekt hinzufügen / bearbeiten</h3>
|
||
<div class="form-grid">
|
||
<div>
|
||
<label>Typ (ID)</label>
|
||
<input id="objType" placeholder="z.B. crate">
|
||
</div>
|
||
<div>
|
||
<label>Name</label>
|
||
<input id="objName" placeholder="z.B. Kiste">
|
||
</div>
|
||
<div>
|
||
<label>Farbe</label>
|
||
<input id="objColor" type="color" value="#8b5a2b">
|
||
</div>
|
||
<div>
|
||
<label>Breite</label>
|
||
<input id="objWidth" type="number" value="32">
|
||
</div>
|
||
<div>
|
||
<label>Höhe</label>
|
||
<input id="objHeight" type="number" value="32">
|
||
</div>
|
||
<div class="checkbox-row">
|
||
<input id="objCollision" type="checkbox">
|
||
<label for="objCollision" style="margin:0;">Kollision</label>
|
||
</div>
|
||
<div class="checkbox-row">
|
||
<input id="objInteractive" type="checkbox">
|
||
<label for="objInteractive" style="margin:0;">Interaktiv</label>
|
||
</div>
|
||
<div class="checkbox-row">
|
||
<input id="objGlowsAtNight" type="checkbox">
|
||
<label for="objGlowsAtNight" style="margin:0;">Leuchtet nachts</label>
|
||
</div>
|
||
<div>
|
||
<label>Aktion (bei interaktiv)</label>
|
||
<input id="objAction" placeholder="z.B. house_storage">
|
||
</div>
|
||
<div>
|
||
<button onclick="saveObject()">Speichern</button>
|
||
</div>
|
||
</div>
|
||
<div style="color:#888; font-size:12px; margin-top:8px;">
|
||
Bekannte Aktionen: <code>loot</code> (einfache Meldung), <code>house_storage</code> (Kiste im Haus),
|
||
<code>house_exit</code> (Ausgang aus dem Haus).
|
||
</div>
|
||
|
||
<h3>Objekt bemalen (optional)</h3>
|
||
<p style="color:#888; font-size:12px; margin-top:0;">
|
||
Statt der reinen Flächenfarbe kannst du hier ein Pixel-Muster malen. Das Raster passt sich
|
||
automatisch an die oben eingestellte Breite/Höhe an. Ohne gemalte Textur wird einfach die
|
||
Flächenfarbe genutzt.
|
||
</p>
|
||
<div style="display:flex; gap:20px; align-items:flex-start; flex-wrap:wrap;">
|
||
<div>
|
||
<canvas id="objPixelCanvas" width="320" height="320" style="border:1px solid #444; cursor:crosshair; image-rendering:pixelated; background:#8b5a2b;"></canvas>
|
||
</div>
|
||
<div style="display:flex; flex-direction:column; gap:8px; min-width:180px;">
|
||
<label style="font-size:12px; color:#999;">Malfarbe</label>
|
||
<input id="objPixelColor" type="color" value="#ffffff" style="width:60px; height:36px; padding:2px; cursor:pointer;">
|
||
|
||
<div style="display:flex; gap:4px; flex-wrap:wrap; margin-top:4px;">
|
||
<button onclick="objPresetBorder()" style="font-size:11px;">Rand</button>
|
||
<button onclick="objPresetCross()" style="font-size:11px;">Kreuz</button>
|
||
</div>
|
||
|
||
<button onclick="rebuildObjPixelGrid()" style="background:#555; margin-top:8px;">Raster an Breite/Höhe anpassen</button>
|
||
<button onclick="clearObjPixelCanvas()" style="background:#a83232;">Löschen (Grundfarbe)</button>
|
||
<button onclick="saveObjTexture()" style="background:#2c7a3d;">Textur speichern</button>
|
||
<button onclick="removeObjTexture()" style="background:#555;">Textur entfernen</button>
|
||
<div class="msg" id="objTextureMsg"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const token = localStorage.getItem("token");
|
||
// Voll-Admin ODER passende Gruppen-Berechtigung reicht jetzt aus - der
|
||
// Server prüft das bei jedem Aufruf ohnehin final ab (siehe requireAdmin);
|
||
// hier reicht ein simpler Login-Check, authFetch() fängt fehlende Rechte
|
||
// beim ersten echten API-Aufruf sauber ab (Meldung + Weiterleitung)
|
||
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;
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// TILES
|
||
// -------------------------------------------------------------
|
||
let allTiles = [];
|
||
|
||
async function loadTiles() {
|
||
const res = await authFetch("/api/admin/tile_config");
|
||
const data = await res.json();
|
||
allTiles = data.tiles || [];
|
||
renderTilesTable();
|
||
}
|
||
|
||
function renderTilesTable() {
|
||
const body = document.getElementById("tilesTableBody");
|
||
body.innerHTML = "";
|
||
|
||
allTiles.forEach(t => {
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td><span class="swatch" style="background:${t.color};"></span>${t.image_data ? " 🎨" : ""}</td>
|
||
<td>${t.id}</td>
|
||
<td>${t.name}</td>
|
||
<td>${t.collision ? "Ja" : "Nein"}</td>
|
||
<td>${t.pvp_safe ? "🛡️ Ja" : "Nein"}</td>
|
||
<td>${t.is_road ? "🛣️ Ja" : "Nein"}</td>
|
||
<td>
|
||
<button onclick="editTile(${t.id})">Bearbeiten</button>
|
||
<button class="danger" onclick="deleteTile(${t.id})">Löschen</button>
|
||
</td>
|
||
`;
|
||
body.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
function editTile(id) {
|
||
const t = allTiles.find(t => t.id === id);
|
||
if (!t) return;
|
||
document.getElementById("tileId").value = t.id;
|
||
document.getElementById("tileName").value = t.name;
|
||
document.getElementById("tileColor").value = t.color;
|
||
document.getElementById("tileCollision").checked = !!t.collision;
|
||
document.getElementById("tilePvpSafe").checked = !!t.pvp_safe;
|
||
document.getElementById("tileIsRoad").checked = !!t.is_road;
|
||
|
||
if (t.image_data) {
|
||
loadTextureIntoGrid(t.image_data);
|
||
} else {
|
||
fillGridWithColor(t.color);
|
||
}
|
||
redrawPixelCanvas();
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// PIXEL-EDITOR FÜR TILE-TEXTUREN
|
||
// -------------------------------------------------------------
|
||
const GRID_SIZE = 32;
|
||
const CELL_PX = 480 / GRID_SIZE; // 15px pro Zelle in der Editor-Anzeige
|
||
let pixelGrid = [];
|
||
const pixelCanvas = document.getElementById("pixelCanvas");
|
||
const pixelCtx = pixelCanvas.getContext("2d");
|
||
let isPainting = false;
|
||
|
||
function fillGridWithColor(color) {
|
||
pixelGrid = [];
|
||
for (let y = 0; y < GRID_SIZE; y++) {
|
||
const row = [];
|
||
for (let x = 0; x < GRID_SIZE; x++) row.push(color);
|
||
pixelGrid.push(row);
|
||
}
|
||
}
|
||
fillGridWithColor(document.getElementById("tileColor").value);
|
||
|
||
function redrawPixelCanvas() {
|
||
for (let y = 0; y < GRID_SIZE; y++) {
|
||
for (let x = 0; x < GRID_SIZE; x++) {
|
||
pixelCtx.fillStyle = pixelGrid[y][x];
|
||
pixelCtx.fillRect(x * CELL_PX, y * CELL_PX, CELL_PX, CELL_PX);
|
||
}
|
||
}
|
||
}
|
||
redrawPixelCanvas();
|
||
|
||
function loadTextureIntoGrid(dataUrl) {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
const off = document.createElement("canvas");
|
||
off.width = GRID_SIZE;
|
||
off.height = GRID_SIZE;
|
||
const offCtx = off.getContext("2d");
|
||
offCtx.drawImage(img, 0, 0, GRID_SIZE, GRID_SIZE);
|
||
const data = offCtx.getImageData(0, 0, GRID_SIZE, GRID_SIZE).data;
|
||
|
||
for (let y = 0; y < GRID_SIZE; y++) {
|
||
for (let x = 0; x < GRID_SIZE; x++) {
|
||
const i = (y * GRID_SIZE + x) * 4;
|
||
const r = data[i], g = data[i + 1], b = data[i + 2];
|
||
pixelGrid[y][x] = `rgb(${r},${g},${b})`;
|
||
}
|
||
}
|
||
redrawPixelCanvas();
|
||
};
|
||
img.src = dataUrl;
|
||
}
|
||
|
||
function paintAt(clientX, clientY) {
|
||
const rect = pixelCanvas.getBoundingClientRect();
|
||
const x = Math.floor((clientX - rect.left) / (rect.width / GRID_SIZE));
|
||
const y = Math.floor((clientY - rect.top) / (rect.height / GRID_SIZE));
|
||
if (x < 0 || y < 0 || x >= GRID_SIZE || y >= GRID_SIZE) return;
|
||
|
||
pixelGrid[y][x] = document.getElementById("pixelColor").value;
|
||
redrawPixelCanvas();
|
||
}
|
||
|
||
pixelCanvas.addEventListener("mousedown", e => { isPainting = true; paintAt(e.clientX, e.clientY); });
|
||
pixelCanvas.addEventListener("mousemove", e => { if (isPainting) paintAt(e.clientX, e.clientY); });
|
||
window.addEventListener("mouseup", () => { isPainting = false; });
|
||
|
||
function clearPixelCanvas() {
|
||
fillGridWithColor(document.getElementById("tileColor").value);
|
||
redrawPixelCanvas();
|
||
}
|
||
|
||
document.getElementById("tileColor").addEventListener("input", e => {
|
||
pixelCanvas.style.background = e.target.value;
|
||
});
|
||
|
||
function presetStripeMiddle() {
|
||
const color = document.getElementById("pixelColor").value;
|
||
const mid = Math.floor(GRID_SIZE / 2);
|
||
for (let y = 0; y < GRID_SIZE; y++) {
|
||
pixelGrid[y][mid - 1] = color;
|
||
pixelGrid[y][mid] = color;
|
||
}
|
||
redrawPixelCanvas();
|
||
}
|
||
|
||
function presetBorder() {
|
||
const color = document.getElementById("pixelColor").value;
|
||
for (let i = 0; i < GRID_SIZE; i++) {
|
||
pixelGrid[0][i] = color;
|
||
pixelGrid[GRID_SIZE - 1][i] = color;
|
||
pixelGrid[i][0] = color;
|
||
pixelGrid[i][GRID_SIZE - 1] = color;
|
||
}
|
||
redrawPixelCanvas();
|
||
}
|
||
|
||
function presetDot() {
|
||
const color = document.getElementById("pixelColor").value;
|
||
const mid = Math.floor(GRID_SIZE / 2);
|
||
for (let y = mid - 1; y <= mid; y++) {
|
||
for (let x = mid - 1; x <= mid; x++) {
|
||
pixelGrid[y][x] = color;
|
||
}
|
||
}
|
||
redrawPixelCanvas();
|
||
}
|
||
|
||
function exportGridAsDataUrl() {
|
||
const off = document.createElement("canvas");
|
||
off.width = GRID_SIZE;
|
||
off.height = GRID_SIZE;
|
||
const offCtx = off.getContext("2d");
|
||
for (let y = 0; y < GRID_SIZE; y++) {
|
||
for (let x = 0; x < GRID_SIZE; x++) {
|
||
offCtx.fillStyle = pixelGrid[y][x];
|
||
offCtx.fillRect(x, y, 1, 1);
|
||
}
|
||
}
|
||
return off.toDataURL("image/png");
|
||
}
|
||
|
||
async function saveTileTexture() {
|
||
const id = Number(document.getElementById("tileId").value);
|
||
const name = document.getElementById("tileName").value.trim();
|
||
const color = document.getElementById("tileColor").value;
|
||
const collision = document.getElementById("tileCollision").checked;
|
||
const pvpSafe = document.getElementById("tilePvpSafe").checked;
|
||
const isRoad = document.getElementById("tileIsRoad").checked;
|
||
|
||
if (isNaN(id) || !name) {
|
||
showTextureMsg("Erst oben ID und Name ausfüllen.", true);
|
||
return;
|
||
}
|
||
|
||
const imageData = exportGridAsDataUrl();
|
||
|
||
const res = await authFetch("/api/admin/tile_config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ id, name, color, collision, pvpSafe, isRoad, imageData })
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
showTextureMsg("Textur gespeichert.");
|
||
await loadTiles();
|
||
} else {
|
||
showTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||
}
|
||
}
|
||
|
||
async function removeTileTexture() {
|
||
const id = Number(document.getElementById("tileId").value);
|
||
const name = document.getElementById("tileName").value.trim();
|
||
const color = document.getElementById("tileColor").value;
|
||
const collision = document.getElementById("tileCollision").checked;
|
||
const pvpSafe = document.getElementById("tilePvpSafe").checked;
|
||
const isRoad = document.getElementById("tileIsRoad").checked;
|
||
|
||
if (isNaN(id) || !name) {
|
||
showTextureMsg("Erst oben ID und Name ausfüllen.", true);
|
||
return;
|
||
}
|
||
|
||
const res = await authFetch("/api/admin/tile_config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ id, name, color, collision, pvpSafe, isRoad, imageData: null })
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
showTextureMsg("Textur entfernt - nutzt jetzt wieder die Grundfarbe.");
|
||
clearPixelCanvas();
|
||
await loadTiles();
|
||
} else {
|
||
showTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||
}
|
||
}
|
||
|
||
function showTextureMsg(text, isError) {
|
||
const el = document.getElementById("textureMsg");
|
||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||
el.textContent = text;
|
||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||
}
|
||
|
||
async function saveTile() {
|
||
const id = Number(document.getElementById("tileId").value);
|
||
const name = document.getElementById("tileName").value.trim();
|
||
const color = document.getElementById("tileColor").value;
|
||
const collision = document.getElementById("tileCollision").checked;
|
||
const pvpSafe = document.getElementById("tilePvpSafe").checked;
|
||
const isRoad = document.getElementById("tileIsRoad").checked;
|
||
|
||
if (isNaN(id) || !name) {
|
||
showTileMsg("ID und Name sind Pflicht.", true);
|
||
return;
|
||
}
|
||
|
||
const res = await authFetch("/api/admin/tile_config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ id, name, color, collision, pvpSafe, isRoad })
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
showTileMsg("Tile gespeichert.");
|
||
document.getElementById("tileId").value = "";
|
||
document.getElementById("tileName").value = "";
|
||
document.getElementById("tileCollision").checked = false;
|
||
document.getElementById("tilePvpSafe").checked = false;
|
||
document.getElementById("tileIsRoad").checked = false;
|
||
await loadTiles();
|
||
} else {
|
||
showTileMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||
}
|
||
}
|
||
|
||
async function deleteTile(id) {
|
||
if (!confirm(`Tile #${id} wirklich löschen?`)) return;
|
||
|
||
const res = await authFetch("/api/admin/tile_config/" + id, { method: "DELETE" });
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
showTileMsg("Tile gelöscht.");
|
||
await loadTiles();
|
||
} else {
|
||
showTileMsg("Löschen fehlgeschlagen.", true);
|
||
}
|
||
}
|
||
|
||
function showTileMsg(text, isError) {
|
||
const el = document.getElementById("tileMsg");
|
||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||
el.textContent = text;
|
||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// OBJECTS
|
||
// -------------------------------------------------------------
|
||
let allObjects = [];
|
||
|
||
async function loadObjects() {
|
||
const res = await authFetch("/api/admin/object_config");
|
||
const data = await res.json();
|
||
allObjects = data.objects || [];
|
||
renderObjectsTable();
|
||
}
|
||
|
||
function renderObjectsTable() {
|
||
const body = document.getElementById("objectsTableBody");
|
||
body.innerHTML = "";
|
||
|
||
allObjects.forEach(o => {
|
||
const tr = document.createElement("tr");
|
||
tr.innerHTML = `
|
||
<td><span class="swatch" style="background:${o.color};"></span>${o.image_data ? " 🎨" : ""}</td>
|
||
<td>${o.type}</td>
|
||
<td>${o.name}</td>
|
||
<td>${o.width} x ${o.height}</td>
|
||
<td>${o.collision ? "Ja" : "Nein"}</td>
|
||
<td>${o.interactive ? "Ja" : "Nein"}</td>
|
||
<td>${o.action || "-"}</td>
|
||
<td>${o.glows_at_night ? "🔆 Ja" : "Nein"}</td>
|
||
<td>
|
||
<button onclick="editObject('${o.type}')">Bearbeiten</button>
|
||
<button class="danger" onclick="deleteObject('${o.type}')">Löschen</button>
|
||
</td>
|
||
`;
|
||
body.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
function editObject(type) {
|
||
const o = allObjects.find(o => o.type === type);
|
||
if (!o) return;
|
||
document.getElementById("objType").value = o.type;
|
||
document.getElementById("objName").value = o.name;
|
||
document.getElementById("objColor").value = o.color;
|
||
document.getElementById("objWidth").value = o.width;
|
||
document.getElementById("objHeight").value = o.height;
|
||
document.getElementById("objCollision").checked = !!o.collision;
|
||
document.getElementById("objInteractive").checked = !!o.interactive;
|
||
document.getElementById("objAction").value = o.action || "";
|
||
document.getElementById("objGlowsAtNight").checked = !!o.glows_at_night;
|
||
|
||
rebuildObjPixelGrid();
|
||
if (o.image_data) {
|
||
loadObjTextureIntoGrid(o.image_data);
|
||
}
|
||
}
|
||
|
||
async function saveObject() {
|
||
const type = document.getElementById("objType").value.trim();
|
||
const name = document.getElementById("objName").value.trim();
|
||
const color = document.getElementById("objColor").value;
|
||
const width = Number(document.getElementById("objWidth").value) || 32;
|
||
const height = Number(document.getElementById("objHeight").value) || 32;
|
||
const collision = document.getElementById("objCollision").checked;
|
||
const interactive = document.getElementById("objInteractive").checked;
|
||
const action = document.getElementById("objAction").value.trim();
|
||
const glowsAtNight = document.getElementById("objGlowsAtNight").checked;
|
||
|
||
if (!type || !name) {
|
||
showObjMsg("Typ und Name sind Pflicht.", true);
|
||
return;
|
||
}
|
||
|
||
const res = await authFetch("/api/admin/object_config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ type, name, color, width, height, collision, interactive, action, glowsAtNight, imageData: exportObjGridAsDataUrl() })
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
showObjMsg("Objekt gespeichert.");
|
||
document.getElementById("objType").value = "";
|
||
document.getElementById("objName").value = "";
|
||
document.getElementById("objAction").value = "";
|
||
document.getElementById("objCollision").checked = false;
|
||
document.getElementById("objInteractive").checked = false;
|
||
document.getElementById("objGlowsAtNight").checked = false;
|
||
clearObjPixelCanvas();
|
||
await loadObjects();
|
||
} else {
|
||
showObjMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||
}
|
||
}
|
||
|
||
async function deleteObject(type) {
|
||
if (!confirm(`Objekt-Typ "${type}" wirklich löschen?`)) return;
|
||
|
||
const res = await authFetch("/api/admin/object_config/" + encodeURIComponent(type), { method: "DELETE" });
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
showObjMsg("Objekt gelöscht.");
|
||
await loadObjects();
|
||
} else {
|
||
showObjMsg("Löschen fehlgeschlagen.", true);
|
||
}
|
||
}
|
||
|
||
function showObjMsg(text, isError) {
|
||
const el = document.getElementById("objMsg");
|
||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||
el.textContent = text;
|
||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// PIXEL-EDITOR FÜR OBJEKT-TEXTUREN
|
||
// -------------------------------------------------------------
|
||
const objCanvas = document.getElementById("objPixelCanvas");
|
||
const objCtx = objCanvas.getContext("2d");
|
||
const OBJ_DISPLAY_MAX = 300; // maximale Anzeigegröße im Editor
|
||
|
||
let objGridW = 32;
|
||
let objGridH = 32;
|
||
let objPixelGrid = [];
|
||
let objCellPxX = 8;
|
||
let objCellPxY = 8;
|
||
let objIsPainting = false;
|
||
|
||
function fillObjGridWithColor(color) {
|
||
objPixelGrid = [];
|
||
for (let y = 0; y < objGridH; y++) {
|
||
const row = [];
|
||
for (let x = 0; x < objGridW; x++) row.push(color);
|
||
objPixelGrid.push(row);
|
||
}
|
||
}
|
||
|
||
function rebuildObjPixelGrid() {
|
||
objGridW = Math.max(4, Math.min(200, parseInt(document.getElementById("objWidth").value) || 32));
|
||
objGridH = Math.max(4, Math.min(200, parseInt(document.getElementById("objHeight").value) || 32));
|
||
|
||
const scale = Math.min(OBJ_DISPLAY_MAX / objGridW, OBJ_DISPLAY_MAX / objGridH, 16);
|
||
objCellPxX = scale;
|
||
objCellPxY = scale;
|
||
|
||
objCanvas.width = objGridW * objCellPxX;
|
||
objCanvas.height = objGridH * objCellPxY;
|
||
|
||
fillObjGridWithColor(document.getElementById("objColor").value);
|
||
redrawObjPixelCanvas();
|
||
}
|
||
rebuildObjPixelGrid();
|
||
|
||
function redrawObjPixelCanvas() {
|
||
for (let y = 0; y < objGridH; y++) {
|
||
for (let x = 0; x < objGridW; x++) {
|
||
objCtx.fillStyle = objPixelGrid[y][x];
|
||
objCtx.fillRect(x * objCellPxX, y * objCellPxY, objCellPxX, objCellPxY);
|
||
}
|
||
}
|
||
}
|
||
|
||
function loadObjTextureIntoGrid(dataUrl) {
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
const off = document.createElement("canvas");
|
||
off.width = objGridW;
|
||
off.height = objGridH;
|
||
const offCtx = off.getContext("2d");
|
||
offCtx.drawImage(img, 0, 0, objGridW, objGridH);
|
||
const data = offCtx.getImageData(0, 0, objGridW, objGridH).data;
|
||
|
||
for (let y = 0; y < objGridH; y++) {
|
||
for (let x = 0; x < objGridW; x++) {
|
||
const i = (y * objGridW + x) * 4;
|
||
objPixelGrid[y][x] = `rgb(${data[i]},${data[i + 1]},${data[i + 2]})`;
|
||
}
|
||
}
|
||
redrawObjPixelCanvas();
|
||
};
|
||
img.src = dataUrl;
|
||
}
|
||
|
||
function objPaintAt(clientX, clientY) {
|
||
const rect = objCanvas.getBoundingClientRect();
|
||
const x = Math.floor((clientX - rect.left) / (rect.width / objGridW));
|
||
const y = Math.floor((clientY - rect.top) / (rect.height / objGridH));
|
||
if (x < 0 || y < 0 || x >= objGridW || y >= objGridH) return;
|
||
|
||
objPixelGrid[y][x] = document.getElementById("objPixelColor").value;
|
||
redrawObjPixelCanvas();
|
||
}
|
||
|
||
objCanvas.addEventListener("mousedown", e => { objIsPainting = true; objPaintAt(e.clientX, e.clientY); });
|
||
objCanvas.addEventListener("mousemove", e => { if (objIsPainting) objPaintAt(e.clientX, e.clientY); });
|
||
window.addEventListener("mouseup", () => { objIsPainting = false; });
|
||
|
||
function clearObjPixelCanvas() {
|
||
fillObjGridWithColor(document.getElementById("objColor").value);
|
||
redrawObjPixelCanvas();
|
||
}
|
||
|
||
document.getElementById("objColor").addEventListener("input", e => {
|
||
objCanvas.style.background = e.target.value;
|
||
});
|
||
|
||
document.getElementById("objWidth").addEventListener("change", rebuildObjPixelGrid);
|
||
document.getElementById("objHeight").addEventListener("change", rebuildObjPixelGrid);
|
||
|
||
function objPresetBorder() {
|
||
const color = document.getElementById("objPixelColor").value;
|
||
for (let x = 0; x < objGridW; x++) {
|
||
objPixelGrid[0][x] = color;
|
||
objPixelGrid[objGridH - 1][x] = color;
|
||
}
|
||
for (let y = 0; y < objGridH; y++) {
|
||
objPixelGrid[y][0] = color;
|
||
objPixelGrid[y][objGridW - 1] = color;
|
||
}
|
||
redrawObjPixelCanvas();
|
||
}
|
||
|
||
function objPresetCross() {
|
||
const color = document.getElementById("objPixelColor").value;
|
||
const midX = Math.floor(objGridW / 2);
|
||
const midY = Math.floor(objGridH / 2);
|
||
for (let x = 0; x < objGridW; x++) objPixelGrid[midY][x] = color;
|
||
for (let y = 0; y < objGridH; y++) objPixelGrid[y][midX] = color;
|
||
redrawObjPixelCanvas();
|
||
}
|
||
|
||
function exportObjGridAsDataUrl() {
|
||
const off = document.createElement("canvas");
|
||
off.width = objGridW;
|
||
off.height = objGridH;
|
||
const offCtx = off.getContext("2d");
|
||
for (let y = 0; y < objGridH; y++) {
|
||
for (let x = 0; x < objGridW; x++) {
|
||
offCtx.fillStyle = objPixelGrid[y][x];
|
||
offCtx.fillRect(x, y, 1, 1);
|
||
}
|
||
}
|
||
return off.toDataURL("image/png");
|
||
}
|
||
|
||
async function saveObjTexture() {
|
||
const type = document.getElementById("objType").value.trim();
|
||
if (!type) {
|
||
showObjTextureMsg("Erst oben einen Typ (ID) eintragen.", true);
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
type,
|
||
name: document.getElementById("objName").value.trim(),
|
||
color: document.getElementById("objColor").value,
|
||
width: Number(document.getElementById("objWidth").value) || 32,
|
||
height: Number(document.getElementById("objHeight").value) || 32,
|
||
collision: document.getElementById("objCollision").checked,
|
||
interactive: document.getElementById("objInteractive").checked,
|
||
action: document.getElementById("objAction").value.trim(),
|
||
glowsAtNight: document.getElementById("objGlowsAtNight").checked,
|
||
imageData: exportObjGridAsDataUrl()
|
||
};
|
||
|
||
if (!payload.name) {
|
||
showObjTextureMsg("Erst oben einen Namen eintragen.", true);
|
||
return;
|
||
}
|
||
|
||
const res = await authFetch("/api/admin/object_config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
showObjTextureMsg("Textur gespeichert.");
|
||
await loadObjects();
|
||
} else {
|
||
showObjTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||
}
|
||
}
|
||
|
||
async function removeObjTexture() {
|
||
const type = document.getElementById("objType").value.trim();
|
||
if (!type) {
|
||
showObjTextureMsg("Erst oben einen Typ (ID) eintragen.", true);
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
type,
|
||
name: document.getElementById("objName").value.trim(),
|
||
color: document.getElementById("objColor").value,
|
||
width: Number(document.getElementById("objWidth").value) || 32,
|
||
height: Number(document.getElementById("objHeight").value) || 32,
|
||
collision: document.getElementById("objCollision").checked,
|
||
interactive: document.getElementById("objInteractive").checked,
|
||
action: document.getElementById("objAction").value.trim(),
|
||
glowsAtNight: document.getElementById("objGlowsAtNight").checked,
|
||
imageData: null
|
||
};
|
||
|
||
const res = await authFetch("/api/admin/object_config", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify(payload)
|
||
});
|
||
const data = await res.json();
|
||
|
||
if (data.ok) {
|
||
showObjTextureMsg("Textur entfernt - nutzt jetzt wieder die Flächenfarbe.");
|
||
clearObjPixelCanvas();
|
||
await loadObjects();
|
||
} else {
|
||
showObjTextureMsg("Fehler: " + (data.error || "unbekannt"), true);
|
||
}
|
||
}
|
||
|
||
function showObjTextureMsg(text, isError) {
|
||
const el = document.getElementById("objTextureMsg");
|
||
el.style.color = isError ? "#e06c6c" : "#6fbf73";
|
||
el.textContent = text;
|
||
setTimeout(() => { el.textContent = ""; }, 4000);
|
||
}
|
||
|
||
// -------------------------------------------------------------
|
||
// INIT
|
||
// -------------------------------------------------------------
|
||
loadTiles();
|
||
loadObjects();
|
||
</script>
|
||
|
||
</body>
|
||
</html> |