Files
2026-08-22 08:40:29 +02:00

346 lines
12 KiB
HTML
Raw Permalink 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>Skin-Editor</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;
max-width: 800px;
}
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; }
.swatch { display: inline-block; width: 32px; height: 32px; border-radius: 4px; vertical-align: middle; margin-right: 6px; border: 1px solid #555; background-size: cover; image-rendering: pixelated; }
input { background: #222; border: 1px solid #444; color: #eee; padding: 6px 8px; border-radius: 4px; font-size: 14px; box-sizing: border-box; }
button { background: #2c7a3d; border: none; color: white; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; }
button.danger { background: #a83232; }
button.secondary { background: #555; }
button:hover { opacity: 0.85; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-top: 12px; }
.form-grid label { display: block; font-size: 12px; color: #999; margin-bottom: 4px; }
#editorArea { display: flex; gap: 24px; align-items: flex-start; flex-wrap: wrap; margin-top: 16px; }
#pixelCanvas { border: 1px solid #444; cursor: crosshair; image-rendering: pixelated; background: #444; }
.msg { font-size: 13px; color: #6fbf73; min-height: 18px; margin-top: 8px; }
.hint { color: #888; font-size: 12px; margin-top: 0; }
</style>
</head>
<body>
<a class="back" href="/index.html">&larr; zurück zur Startseite</a>
<h1>🎨 Skin-Editor</h1>
<p class="hint">Skins werden mit exakt 20×20 Pixeln gemalt (dieselbe Größe wie der Körper-Kasten im Spiel) und darüber gezeichnet, statt nur ein Emoji zu sein.</p>
<div class="panel">
<table>
<thead><tr><th></th><th>Name</th><th></th></tr></thead>
<tbody id="itemsTableBody"></tbody>
</table>
<div class="hint" id="itemsEmpty" style="display:none;">Noch keine Skins angelegt.</div>
<h3 style="margin-top:24px;">Skin anlegen / bearbeiten</h3>
<div class="form-grid">
<div>
<label>Name</label>
<input id="itemName" placeholder="z.B. Punk-Frisur">
</div>
<div>
<button onclick="newItemForm()" class="secondary">Neu (Formular leeren)</button>
</div>
</div>
<div id="editorArea">
<div>
<canvas id="pixelCanvas" width="320" height="320"></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="#f1c27d" style="width:60px; height:36px; padding:2px; cursor:pointer;">
<div style="display:flex; gap:4px; flex-wrap:wrap; margin-top:4px;">
<button onclick="presetFace()" style="font-size:11px;">Gesicht-Grundform</button>
</div>
<button onclick="clearPixelCanvas()" class="danger" style="margin-top:8px;">Löschen (transparent)</button>
<button onclick="saveItem()">Speichern</button>
<button onclick="deleteCurrentItem()" class="danger" id="deleteBtn" style="display:none;">Löschen</button>
<div class="msg" id="itemMsg"></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;
}
function escapeHtml(str) {
const div = document.createElement("div");
div.textContent = str;
return div.innerHTML;
}
const GRID = 20; // exakt Körper-Kasten-Größe im Spiel
const SCALE = 16;
let allItems = [];
let currentEditId = null;
let pixelGrid = [];
const canvas = document.getElementById("pixelCanvas");
const ctx = canvas.getContext("2d");
canvas.width = GRID * SCALE;
canvas.height = GRID * SCALE;
function fillGridTransparent() {
pixelGrid = [];
for (let y = 0; y < GRID; y++) {
const row = [];
for (let x = 0; x < GRID; x++) row.push(null); // null = transparent
pixelGrid.push(row);
}
}
function redrawCanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Schachbrett-Muster als Transparenz-Hintergrund
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
if (pixelGrid[y][x]) {
ctx.fillStyle = pixelGrid[y][x];
ctx.fillRect(x * SCALE, y * SCALE, SCALE, SCALE);
} else {
ctx.fillStyle = (x + y) % 2 === 0 ? "#3a3a3a" : "#2a2a2a";
ctx.fillRect(x * SCALE, y * SCALE, SCALE, SCALE);
}
}
}
}
function loadTextureIntoGrid(dataUrl) {
const img = new Image();
img.onload = () => {
const off = document.createElement("canvas");
off.width = GRID;
off.height = GRID;
const offCtx = off.getContext("2d");
offCtx.drawImage(img, 0, 0, GRID, GRID);
const data = offCtx.getImageData(0, 0, GRID, GRID).data;
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
const i = (y * GRID + x) * 4;
const alpha = data[i + 3];
pixelGrid[y][x] = alpha > 10 ? `rgb(${data[i]},${data[i + 1]},${data[i + 2]})` : null;
}
}
redrawCanvas();
};
img.src = dataUrl;
}
function exportGridAsDataUrl() {
const off = document.createElement("canvas");
off.width = GRID;
off.height = GRID;
const offCtx = off.getContext("2d");
for (let y = 0; y < GRID; y++) {
for (let x = 0; x < GRID; x++) {
if (pixelGrid[y][x]) {
offCtx.fillStyle = pixelGrid[y][x];
offCtx.fillRect(x, y, 1, 1);
}
}
}
return off.toDataURL("image/png");
}
let isPainting = false;
let isErasing = false;
function paintAt(clientX, clientY, erase) {
const rect = canvas.getBoundingClientRect();
const x = Math.floor((clientX - rect.left) / (rect.width / GRID));
const y = Math.floor((clientY - rect.top) / (rect.height / GRID));
if (x < 0 || y < 0 || x >= GRID || y >= GRID) return;
pixelGrid[y][x] = erase ? null : document.getElementById("pixelColor").value;
redrawCanvas();
}
canvas.addEventListener("mousedown", e => {
isPainting = true;
isErasing = e.button === 2;
paintAt(e.clientX, e.clientY, isErasing);
});
canvas.addEventListener("mousemove", e => { if (isPainting) paintAt(e.clientX, e.clientY, isErasing); });
canvas.addEventListener("contextmenu", e => e.preventDefault());
window.addEventListener("mouseup", () => { isPainting = false; });
function clearPixelCanvas() {
fillGridTransparent();
redrawCanvas();
}
function presetFace() {
// Einfache Kopf-Grundform als Startpunkt (Kreis-artig, Rest bleibt transparent)
const skin = document.getElementById("pixelColor").value;
for (let y = 3; y < 15; y++) {
for (let x = 4; x < 16; x++) {
pixelGrid[y][x] = skin;
}
}
redrawCanvas();
}
// -------------------------------------------------------------
// LADEN / ANZEIGEN
// -------------------------------------------------------------
async function loadItems() {
const res = await authFetch("/api/admin/skin_items");
const data = await res.json();
allItems = data.items || [];
renderItemsTable();
}
function renderItemsTable() {
const body = document.getElementById("itemsTableBody");
const empty = document.getElementById("itemsEmpty");
body.innerHTML = "";
empty.style.display = allItems.length === 0 ? "block" : "none";
allItems.forEach(item => {
const tr = document.createElement("tr");
tr.innerHTML = `
<td><span class="swatch"></span></td>
<td>${escapeHtml(item.name)}</td>
<td>
<button onclick="editItem(${item.id})">Bearbeiten</button>
<button class="danger" onclick="quickDelete(${item.id})">Löschen</button>
</td>
`;
body.appendChild(tr);
});
}
function newItemForm() {
currentEditId = null;
document.getElementById("itemName").value = "";
document.getElementById("deleteBtn").style.display = "none";
fillGridTransparent();
redrawCanvas();
}
async function editItem(id) {
const item = allItems.find(i => i.id === id);
if (!item) return;
const res = await authFetch("/api/admin/skin_items/" + id);
const data = await res.json();
if (!data.ok) return;
currentEditId = id;
document.getElementById("itemName").value = data.item.name;
document.getElementById("deleteBtn").style.display = "inline-block";
fillGridTransparent();
redrawCanvas();
if (data.item.image_data) loadTextureIntoGrid(data.item.image_data);
}
async function saveItem() {
const name = document.getElementById("itemName").value.trim();
if (!name) {
showMsg("Name ist Pflicht.", true);
return;
}
const payload = { id: currentEditId, name, imageData: exportGridAsDataUrl() };
const res = await authFetch("/api/admin/skin_items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.ok) {
showMsg("Gespeichert.");
await loadItems();
newItemForm();
} else {
showMsg("Fehler: " + (data.error || "unbekannt"), true);
}
}
async function deleteCurrentItem() {
if (currentEditId === null) return;
await quickDelete(currentEditId);
newItemForm();
}
async function quickDelete(id) {
if (!confirm("Diesen Skin wirklich löschen?")) return;
const res = await authFetch("/api/admin/skin_items/" + id, { method: "DELETE" });
const data = await res.json();
if (data.ok) {
showMsg("Gelöscht.");
await loadItems();
} else {
showMsg("Löschen fehlgeschlagen.", true);
}
}
function showMsg(text, isError) {
const el = document.getElementById("itemMsg");
el.style.color = isError ? "#e06c6c" : "#6fbf73";
el.textContent = text;
setTimeout(() => { el.textContent = ""; }, 4000);
}
// -------------------------------------------------------------
// INIT
// -------------------------------------------------------------
fillGridTransparent();
redrawCanvas();
loadItems();
</script>
</body>
</html>