Erster Commit

This commit is contained in:
2026-08-22 08:40:29 +02:00
commit 875477d425
1961 changed files with 930336 additions and 0 deletions
+902
View File
@@ -0,0 +1,902 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Tiles &amp; 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">&larr; zurück zur Startseite</a>
<h1>🧱 Tiles &amp; 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>