Files
bordervillegame/admin_tiles.html
T
2026-08-22 08:40:29 +02:00

902 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>