Private
Public Access
1
0

fix
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m43s

This commit is contained in:
Sebastian Unterschütz
2025-12-05 21:56:44 +01:00
parent ae3eb34c0e
commit 141f74c6ad
9 changed files with 944 additions and 391 deletions

View File

@@ -2,319 +2,261 @@
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Ultimate Chunk Editor (+Templates)</title>
<title>Level Chunk Editor</title>
<style>
body { background: #1a1a1a; color: #ddd; font-family: 'Segoe UI', monospace; display: flex; height: 100vh; margin: 0; overflow: hidden; }
/* Sidebar */
#sidebar {
width: 340px; background: #2a2a2a; padding: 15px; border-right: 2px solid #444;
display: flex; flex-direction: column; gap: 10px; overflow-y: auto;
width: 300px; background: #2a2a2a; padding: 15px; border-right: 1px solid #444;
display: flex; flex-direction: column; gap: 12px; overflow-y: auto;
box-shadow: 2px 0 10px rgba(0,0,0,0.5);
}
/* Canvas Bereich */
#canvas-wrapper {
flex: 1; overflow: auto; position: relative; background: #333;
/* Karomuster */
background-image: linear-gradient(45deg, #252525 25%, transparent 25%), linear-gradient(-45deg, #252525 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #252525 75%), linear-gradient(-45deg, transparent 75%, #252525 75%);
background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
canvas { background: rgba(0,0,0,0.2); cursor: crosshair; display: block; margin-top: 50px; }
canvas {
background: rgba(0,0,0,0.2); cursor: crosshair; display: block; margin-top: 50px;
}
.tool-btn { padding: 8px; background: #444; border: 1px solid #555; color: white; cursor: pointer; text-align: left; border-radius: 4px; font-size: 12px; }
/* UI Elemente */
.panel { background: #333; padding: 10px; border: 1px solid #444; border-radius: 4px; }
.tools-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
.tool-btn {
padding: 8px; background: #444; border: 1px solid #555; color: white;
cursor: pointer; text-align: left; border-radius: 4px; font-size: 11px;
display: flex; align-items: center; gap: 8px; overflow: hidden; white-space: nowrap;
}
.tool-btn:hover { background: #555; filter: brightness(1.2); }
.tool-btn.active { background: #ffcc00; color: black; font-weight: bold; border-color: #ffcc00; }
.tool-btn:hover:not(.active) { background: #555; }
.tool-thumb { width: 20px; height: 20px; object-fit: contain; background: rgba(0,0,0,0.3); border-radius: 3px; }
.panel { background: #333; padding: 10px; border: 1px solid #444; border-radius: 4px; margin-bottom: 5px; }
.group-title { font-size: 10px; color: #2196F3; font-weight: bold; margin-bottom: 5px; text-transform: uppercase; border-bottom: 1px solid #444; padding-bottom: 2px; }
input, select { width: 100%; padding: 4px; background: #1a1a1a; color: white; border: 1px solid #555; margin-bottom: 5px; box-sizing: border-box; border-radius: 3px; font-size: 11px; }
input, select {
width: 100%; padding: 5px; background: #1a1a1a; color: white; border: 1px solid #555;
margin-bottom: 5px; box-sizing: border-box; border-radius: 3px; font-size: 11px;
}
input:focus { border-color: #ffcc00; outline: none; }
.row { display: flex; gap: 5px; }
label { font-size: 10px; color: #aaa; display: block; margin-bottom: 1px;}
h3 { margin: 10px 0 5px 0; color: #ffcc00; border-bottom: 1px solid #555; padding-bottom: 5px; font-size: 14px;}
h3 { margin: 5px 0 5px 0; color: #ffcc00; border-bottom: 1px solid #555; padding-bottom: 5px; font-size: 13px;}
button.action { width:100%; border:none; color:white; cursor:pointer; padding: 8px; margin-top:5px; border-radius:3px; font-weight:bold;}
/* Template Liste Style */
.template-item { display: flex; justify-content: space-between; align-items: center; background: #222; padding: 5px; margin-bottom: 2px; font-size: 11px; border: 1px solid #444; cursor: pointer; }
.template-item:hover { background: #444; }
.template-del { color: #f44336; font-weight: bold; padding: 0 5px; cursor: pointer; }
button.action { width:100%; border:none; color:white; cursor:pointer; padding: 10px; margin-top:5px; border-radius:3px; font-weight:bold;}
.btn-load { background: #2196F3; }
.btn-save { background: #4caf50; }
.btn-del { background: #f44336; margin-top: 10px;}
</style>
</head>
<body>
<div id="sidebar">
<h3>🎨 VORLAGEN</h3>
<h3>📂 LEVEL LADEN</h3>
<div class="panel">
<div style="display:flex; gap:5px;">
<input type="text" id="tpl-name" placeholder="Name (z.B. Teacher Hut)" style="margin:0;">
<button onclick="saveTemplate()" style="background:#4caf50; border:none; color:white; cursor:pointer; width:40px;">💾</button>
<select id="chunk-select"><option value="">Lade Liste...</option></select>
<div class="row">
<button class="action btn-load" onclick="loadSelectedChunk()">📂 Laden</button>
<button class="action" onclick="loadConfig()" style="background:#444; width:40px;">🔄</button>
</div>
<div id="template-list" style="max-height: 100px; overflow-y: auto; margin-top: 5px; border: 1px solid #444;">
</div>
<button class="action" onclick="applyTemplate()" style="background:#2196F3; font-size: 11px;">✨ Auf Selektion anwenden</button>
</div>
<h3>🛠 TOOLS</h3>
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:5px;">
<h3>🛠 WERKZEUGE (ASSETS)</h3>
<div class="tools-grid" id="tools-container">
<button class="tool-btn active" onclick="setTool('select')">👆 Select</button>
<button class="tool-btn" onclick="setTool('platform')">🧱 Platform</button>
<button class="tool-btn" onclick="setTool('teacher')">👨‍🏫 Teacher</button>
<button class="tool-btn" onclick="setTool('principal')">👿 Boss</button>
<button class="tool-btn" onclick="setTool('coin')">🪙 Coin</button>
<button class="tool-btn" onclick="setTool('powerup')">⚡ PowerUp</button>
</div>
<h3>⚙️ EIGENSCHAFTEN</h3>
<div id="props" class="panel" style="display:none;">
<div class="group-title">Basis & Physik</div>
<div class="row">
<div style="flex:2"><label>ID</label><input type="text" id="prop-id" oninput="updateProp()"></div>
<div style="flex:1"><label>Type</label><input type="text" id="prop-type" readonly style="color:#666;"></div>
</div>
<div class="row"><div style="flex:2"><label>ID</label><input type="text" id="prop-id" readonly style="color:#777"></div></div>
<div class="row">
<div style="flex:1"><label>X</label><input type="number" id="prop-x" oninput="updateProp()"></div>
<div style="flex:1"><label>Y</label><input type="number" id="prop-y" oninput="updateProp()"></div>
</div>
<div class="row">
<div style="flex:1"><label>Width</label><input type="number" id="prop-w" oninput="updateProp()"></div>
<div style="flex:1"><label>Height</label><input type="number" id="prop-h" oninput="updateProp()"></div>
<div style="flex:1"><label>Breite</label><input type="number" id="prop-w" oninput="updateProp()"></div>
<div style="flex:1"><label>Höhe</label><input type="number" id="prop-h" oninput="updateProp()"></div>
</div>
<div class="group-title" style="margin-top:10px;">🖼️ Optik (Textur)</div>
<label>Bild Datei (../../assets/)</label>
<input type="text" id="prop-image" oninput="updateProp()">
<div class="row">
<div style="flex:1"><label>Scale</label><input type="number" step="0.1" id="prop-scale" oninput="updateProp()"></div>
<div style="flex:1"><label>Farbe</label><input type="color" id="prop-color" oninput="updateProp()" style="height:25px; padding:0;"></div>
</div>
<div class="row">
<div style="flex:1"><label>Off X</label><input type="number" id="prop-imgx" oninput="updateProp()"></div>
<div style="flex:1"><label>Off Y</label><input type="number" id="prop-imgy" oninput="updateProp()"></div>
</div>
<button class="action" onclick="deleteSelected()" style="background:#f44336; margin-top:10px;">🗑 Löschen</button>
<button class="action btn-del" onclick="deleteSelected()">🗑 Entfernen</button>
</div>
<div style="margin-top:auto;">
<h3>💾 LEVEL DATEI</h3>
<div class="panel">
<select id="chunk-select"><option value="">Lade Liste...</option></select>
<div class="row">
<button class="action" onclick="loadSelectedChunk()" style="background:#2196F3; margin-top:2px;">📂 Laden</button>
<button class="action" onclick="refreshList()" style="background:#444; margin-top:2px;">🔄</button>
</div>
<h3>💾 SPEICHERN</h3>
<label>Chunk Name (ID)</label>
<input type="text" id="chunk-name" value="new_chunk">
<label style="margin-top:5px">Chunk Name</label>
<input type="text" id="chunk-name" value="level_01">
<label>Länge</label>
<input type="number" id="chunk-width" value="2000">
<label>Länge (Pixel)</label>
<input type="number" id="chunk-width" value="2000">
<button class="action" onclick="saveChunk()" style="background:#4caf50;">💾 Speichern (DB)</button>
<button class="action" onclick="exportJSON()" style="background:#FF9800;">📋 JSON Copy</button>
</div>
<button class="action btn-save" onclick="saveChunk()">💾 In DB Speichern</button>
</div>
</div>
<div id="canvas-wrapper">
<canvas id="editorCanvas" width="4000" height="500"></canvas>
<canvas id="editorCanvas"></canvas>
<div style="position:absolute; bottom:5px; right:5px; color:white; font-size:10px; opacity:0.5; pointer-events:none;">
Mausrad: Scrollen | Drag: Bewegen
</div>
</div>
<script>
// --- STATE ---
const canvas = document.getElementById('editorCanvas');
const ctx = canvas.getContext('2d');
const ctx = canvas.getContext('2d', { alpha: false });
// State
let elements = [];
let selectedElement = null;
let currentTool = 'select';
// Config & Assets
let loadedConfig = null;
let TOOL_DEFS = {};
const imageCache = {};
let dpr = 1;
// Dragging Logic
let isDragging = false;
let dragStart = {x:0, y:0};
const imageCache = {};
let loadedConfig = null;
// --- TEMPLATE SYSTEM (NEU) ---
let myTemplates = JSON.parse(localStorage.getItem('editor_templates') || '{}');
let selectedTemplateKey = null;
function renderTemplateList() {
const list = document.getElementById('template-list');
list.innerHTML = "";
if (Object.keys(myTemplates).length === 0) {
list.innerHTML = "<div style='color:#666; padding:5px; font-style:italic;'>Keine Vorlagen</div>";
return;
}
for (let key in myTemplates) {
const div = document.createElement('div');
div.className = 'template-item';
if (selectedTemplateKey === key) div.style.background = "#2196F3";
div.innerHTML = `<span>${key}</span>`;
// Delete Btn
const del = document.createElement('span');
del.className = 'template-del';
del.innerText = '×';
del.onclick = (e) => { e.stopPropagation(); deleteTemplate(key); };
div.appendChild(del);
div.onclick = () => { selectedTemplateKey = key; renderTemplateList(); };
list.appendChild(div);
}
}
function saveTemplate() {
if (!selectedElement) { alert("Bitte erst ein Objekt auswählen, um dessen Werte zu speichern."); return; }
const name = document.getElementById('tpl-name').value;
if (!name) { alert("Bitte Namen eingeben"); return; }
// Wir speichern nur die visuellen/hitbox Eigenschaften, NICHT die Position!
const tpl = {
w: selectedElement.w,
h: selectedElement.h,
color: selectedElement.color,
image: selectedElement.image,
imgScale: selectedElement.imgScale,
imgOffsetX: selectedElement.imgOffsetX,
imgOffsetY: selectedElement.imgOffsetY,
type: selectedElement.type,
id: selectedElement.id // ID auch übernehmen (z.B. "teacher_hard")
};
myTemplates[name] = tpl;
localStorage.setItem('editor_templates', JSON.stringify(myTemplates));
document.getElementById('tpl-name').value = "";
renderTemplateList();
}
function applyTemplate() {
if (!selectedElement) { alert("Bitte ein Objekt auswählen."); return; }
if (!selectedTemplateKey || !myTemplates[selectedTemplateKey]) { alert("Bitte eine Vorlage auswählen."); return; }
const tpl = myTemplates[selectedTemplateKey];
// Werte übertragen
selectedElement.w = tpl.w;
selectedElement.h = tpl.h;
selectedElement.color = tpl.color;
selectedElement.image = tpl.image;
selectedElement.imgScale = tpl.imgScale;
selectedElement.imgOffsetX = tpl.imgOffsetX;
selectedElement.imgOffsetY = tpl.imgOffsetY;
// ID und Type nur ändern wenn gewünscht, wir lassen Type meist gleich
// selectedElement.id = tpl.id;
updateUI();
draw();
}
function deleteTemplate(key) {
if(!confirm(`Vorlage '${key}' löschen?`)) return;
delete myTemplates[key];
localStorage.setItem('editor_templates', JSON.stringify(myTemplates));
if(selectedTemplateKey === key) selectedTemplateKey = null;
renderTemplateList();
}
// --- DEFAULTS ---
const DEFAULTS = {
platform: { w: 150, h: 20, color: '#8B4513', type: 'platform', id: 'plat', image: '' },
teacher: { w: 30, h: 60, color: '#000080', type: 'teacher', id: 'teacher', image: 'teacher1.png' },
principal: { w: 40, h: 70, color: '#000000', type: 'teacher', id: 'principal', image: 'principal1.png' },
coin: { w: 20, h: 20, color: '#FFD700', type: 'coin', id: 'coin0', image: 'coin1.png' },
powerup: { w: 30, h: 30, color: '#00FF00', type: 'powerup', id: 'p_boot', image: 'powerup_boot1.png' }
};
// --- INIT ---
renderTemplateList(); // Templates laden
refreshList();
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
loadConfig();
requestAnimationFrame(renderLoop);
// --- HELPER ---
function getImage(path) {
if (!path) return null;
if (imageCache[path]) return imageCache[path];
const img = new Image();
img.src = "../../assets/" + path;
img.onerror = () => { img.src = "../../assets/" + path; };
imageCache[path] = img;
return img;
function resizeCanvas() {
const container = canvas.parentElement;
dpr = window.devicePixelRatio || 1;
// Interne Größe
canvas.width = container.clientWidth * dpr;
canvas.height = container.clientHeight * dpr;
// CSS Größe
canvas.style.width = container.clientWidth + 'px';
canvas.style.height = container.clientHeight + 'px';
ctx.scale(dpr, dpr);
}
function renderLoop() { draw(); requestAnimationFrame(renderLoop); }
// --- 1. CONFIG & ASSETS LADEN ---
async function loadConfig() {
try {
const res = await fetch('/api/config');
const data = await res.json();
function draw() {
ctx.clearRect(0,0,canvas.width, canvas.height);
// Normalize Data
loadedConfig = {
obstacles: data.obstacles || data.Obstacles || [],
chunks: data.chunks || data.Chunks || []
};
// Grid
ctx.fillStyle = "rgba(0,0,0,0.5)"; ctx.fillRect(0, 350, canvas.width, 50);
ctx.strokeStyle = "#aaa"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, 350); ctx.lineTo(canvas.width, 350); ctx.stroke();
buildToolPalette();
buildChunkList();
// Start Zone
ctx.fillStyle = "rgba(0,255,0,0.05)"; ctx.fillRect(0, 0, 800, 350);
ctx.fillStyle = "#aaa"; ctx.font = "12px Arial"; ctx.fillText("Viewport Start (800px)", 10, 20);
} catch(e) {
console.error("Config Fehler:", e);
alert("Konnte Config nicht laden.");
}
}
elements.forEach(el => {
const w = el.w; const h = el.h; const x = el.x; const y = el.y;
const scale = el.imgScale || 1.0;
const offX = el.imgOffsetX || 0;
const offY = el.imgOffsetY || 0;
const imgPath = el.image;
function buildToolPalette() {
const container = document.getElementById('tools-container');
// Reset (Select & Platform behalten)
container.innerHTML = `
<button class="tool-btn active" onclick="setTool('select')">👆 Select</button>
<button class="tool-btn" onclick="setTool('platform')">🧱 Platform</button>
`;
// Hitbox
ctx.fillStyle = el.color || '#888';
if (imgPath) ctx.fillStyle = "rgba(255, 0, 0, 0.2)";
ctx.fillRect(x, y, w, h);
// Default Platform Def
TOOL_DEFS['platform'] = { type: 'platform', id: 'plat', w: 150, h: 20, color: '#8B4513' };
if(el === selectedElement) {
ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.setLineDash([5, 3]);
} else {
ctx.strokeStyle = "rgba(0,0,0,0.3)"; ctx.lineWidth = 1; ctx.setLineDash([]);
}
ctx.strokeRect(x, y, w, h);
// Assets hinzufügen
loadedConfig.obstacles.forEach(obs => {
// Speichern in Defs für schnellen Zugriff
TOOL_DEFS[obs.id] = obs;
// Bild
const img = getImage(imgPath);
if (img && img.complete && img.naturalHeight !== 0) {
const drawW = w * scale;
const drawH = h * scale;
const baseX = x + (w - drawW) / 2;
const baseY = y + (h - drawH);
const finalX = baseX + offX;
const finalY = baseY + offY;
const btn = document.createElement('button');
btn.className = 'tool-btn';
btn.onclick = () => setTool(obs.id);
ctx.drawImage(img, finalX, finalY, drawW, drawH);
if(el === selectedElement) {
ctx.strokeStyle = "#2196F3"; ctx.lineWidth = 1; ctx.setLineDash([]);
ctx.strokeRect(finalX, finalY, drawW, drawH);
}
}
// Label
ctx.fillStyle = "white"; ctx.font = "bold 10px sans-serif";
ctx.shadowColor="black"; ctx.shadowBlur=3;
ctx.fillText(el.id, x, y - 4);
ctx.shadowBlur=0;
const imgPath = obs.image ? `../assets/${obs.image}` : '';
btn.innerHTML = `
<img src="${imgPath}" class="tool-thumb" onerror="this.style.display='none'">
${obs.id}
`;
container.appendChild(btn);
});
}
// --- INTERACTION ---
canvas.addEventListener('mousedown', e => {
function buildChunkList() {
const sel = document.getElementById('chunk-select');
sel.innerHTML = "<option value=''>Bitte wählen...</option>";
loadedConfig.chunks.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.innerText = `${c.id} (${c.totalWidth || '?'}px)`;
sel.appendChild(opt);
});
}
// --- 2. TOOL LOGIK ---
function setTool(id) {
currentTool = id;
selectedElement = null;
updateUI();
// Buttons highlighten
document.querySelectorAll('.tool-btn').forEach(btn => btn.classList.remove('active'));
// Einfacher Hack um den Button zu finden (da wir keine IDs auf Buttons haben)
// In Produktion besser: data-id Attribut nutzen
const btns = document.querySelectorAll('.tool-btn');
for(let b of btns) {
if(b.innerText.includes(id) || (id==='select' && b.innerText.includes('Select'))) {
b.classList.add('active');
break;
}
}
}
// --- 3. INTERAKTION (Maus) ---
function getMousePos(e) {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
return {
x: (e.clientX - rect.left), // CSS Pixel (wegen scale(dpr) brauchen wir hier nicht *dpr)
y: (e.clientY - rect.top)
};
}
canvas.addEventListener('mousedown', e => {
const m = getMousePos(e);
// Scroll Offset des Canvas abziehen?
// Aktuell scrollt das Canvas nicht (Breite 4000px).
// Wir nehmen an, das Canvas passt in den Container und hat Overflow:Auto im Wrapper.
// Der Event Listener ist auf dem Canvas, also gibt clientX relative Koordinaten.
// Aber Moment: Wenn das Canvas 4000px breit ist, und wir scrollen, ändern sich clientX nicht relativ zum Canvas.
// KORREKTUR: Wir müssen scrollLeft addieren wenn das Canvas im Wrapper scrollt?
// Nein, e.offsetX/Y ist relativ zum Element.
// Besser:
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left; // Position im Canvas
const my = e.clientY - rect.top;
if (currentTool === 'select') {
// Selektieren (Rückwärts, oberstes zuerst)
selectedElement = null;
for(let i=elements.length-1; i>=0; i--) {
for (let i = elements.length - 1; i >= 0; i--) {
const el = elements[i];
if(mx >= el.x && mx <= el.x + el.w && my >= el.y && my <= el.y + el.h) {
if (mx >= el.x && mx <= el.x + el.w && my >= el.y && my <= el.y + el.h) {
selectedElement = el;
isDragging = true;
dragStart = { x: mx - el.x, y: my - el.y };
@@ -322,158 +264,192 @@
}
}
} else {
const def = DEFAULTS[currentTool];
if(def) {
// Neues Objekt platzieren
const def = TOOL_DEFS[currentTool];
if (def) {
const newEl = {
type: def.type, id: def.id,
x: Math.floor(mx/10)*10, y: Math.floor(my/10)*10,
w: def.w, h: def.h, color: def.color,
image: def.image,
imgScale: 1.0, imgOffsetX: 0, imgOffsetY: 0
id: def.id,
type: def.type,
// Snap to Grid 10px
x: Math.floor(mx / 10) * 10,
y: Math.floor(my / 10) * 10,
w: def.width || def.w || 30, // Fallback
h: def.height || def.h || 30,
// Speichere Referenz auf Def (für Rendering), aber nicht in JSON später
_def: def
};
// --- AUTO APPLY TEMPLATE? ---
// Optional: Wenn man ein Template ausgewählt hat, könnte man es direkt anwenden.
// Das machen wir aber lieber manuell per "Apply".
elements.push(newEl);
selectedElement = newEl;
currentTool = 'select';
document.querySelectorAll('.tool-btn')[0].click();
// Auto-Switch zu Select
setTool('select');
}
}
updateUI();
});
canvas.addEventListener('mousemove', e => {
if(isDragging && selectedElement) {
if (isDragging && selectedElement) {
const rect = canvas.getBoundingClientRect();
const rawX = e.clientX - rect.left - dragStart.x;
const rawY = e.clientY - rect.top - dragStart.y;
selectedElement.x = Math.floor(rawX / 10) * 10;
selectedElement.y = Math.floor(rawY / 10) * 10;
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
selectedElement.x = Math.floor((mx - dragStart.x) / 10) * 10;
selectedElement.y = Math.floor((my - dragStart.y) / 10) * 10;
updateUI();
}
});
window.addEventListener('mouseup', () => { isDragging = false; });
// --- UI UPDATES ---
window.addEventListener('mouseup', () => isDragging = false);
// --- 4. RENDERING ---
function getImage(path) {
if (!path) return null;
if (imageCache[path]) return imageCache[path];
const img = new Image();
img.src = "../assets/" + path;
imageCache[path] = img;
return img;
}
function renderLoop() {
// Logische Größe (CSS Pixel)
const w = canvas.width / dpr;
const h = canvas.height / dpr;
const groundY = 350;
ctx.clearRect(0,0,w,h);
// Hintergrund
ctx.fillStyle = "#222"; ctx.fillRect(0, 0, w, h);
// Boden
ctx.fillStyle = "rgba(255,255,255,0.1)"; ctx.fillRect(0, groundY, w, 50);
ctx.strokeStyle = "#666"; ctx.beginPath(); ctx.moveTo(0, groundY); ctx.lineTo(w, groundY); ctx.stroke();
// Startbereich Markierung
ctx.fillStyle = "rgba(0, 255, 0, 0.05)"; ctx.fillRect(0, 0, 800, 400);
ctx.fillStyle = "#666"; ctx.font="10px monospace"; ctx.fillText("VIEWPORT START (800px)", 10, 20);
// Elemente
elements.forEach(el => {
// Def laden falls nicht da (passiert beim Laden)
if (!el._def && TOOL_DEFS[el.id]) el._def = TOOL_DEFS[el.id];
// Fallback Def
const def = el._def || { color: '#f0f', imgScale: 1, imgOffsetX:0, imgOffsetY:0 };
// 1. HITBOX
ctx.fillStyle = def.color || '#888';
// Wenn Bild da ist, Hitbox transparent
const imgPath = def.image;
if (imgPath) ctx.globalAlpha = 0.3;
ctx.fillRect(el.x, el.y, el.w, el.h);
ctx.globalAlpha = 1.0;
// Rahmen
if (el === selectedElement) {
ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.setLineDash([4, 2]);
} else {
ctx.strokeStyle = "rgba(0,0,0,0.5)"; ctx.lineWidth = 1; ctx.setLineDash([]);
}
ctx.strokeRect(el.x, el.y, el.w, el.h);
// 2. BILD (Smart Rendering wie im Spiel)
if (imgPath) {
const img = getImage(imgPath);
if (img && img.complete && img.naturalWidth !== 0) {
const scale = def.imgScale || 1.0;
const offX = def.imgOffsetX || 0;
const offY = def.imgOffsetY || 0;
const dW = el.w * scale;
const dH = el.h * scale;
const bX = el.x + (el.w - dW)/2 + offX;
const bY = el.y + (el.h - dH) + offY;
ctx.drawImage(img, bX, bY, dW, dH);
}
}
// Label
ctx.fillStyle = "white"; ctx.font="10px sans-serif";
ctx.fillText(el.id, el.x, el.y - 4);
});
requestAnimationFrame(renderLoop);
}
// --- 5. UI UPDATES ---
function updateUI() {
const props = document.getElementById('props');
if(selectedElement) {
props.style.display = 'block';
const set = (id, val) => document.getElementById(id).value = val;
set('prop-id', selectedElement.id || '');
set('prop-type', selectedElement.type || '');
set('prop-x', selectedElement.x);
set('prop-y', selectedElement.y);
set('prop-w', selectedElement.w);
set('prop-h', selectedElement.h);
set('prop-image', selectedElement.image || '');
set('prop-scale', selectedElement.imgScale || 1.0);
set('prop-color', selectedElement.color || '#888888');
set('prop-imgx', selectedElement.imgOffsetX || 0);
set('prop-imgy', selectedElement.imgOffsetY || 0);
const p = document.getElementById('props');
if (selectedElement) {
p.style.display = 'block';
document.getElementById('prop-id').value = selectedElement.id;
document.getElementById('prop-x').value = selectedElement.x;
document.getElementById('prop-y').value = selectedElement.y;
document.getElementById('prop-w').value = selectedElement.w;
document.getElementById('prop-h').value = selectedElement.h;
} else {
props.style.display = 'none';
p.style.display = 'none';
}
}
function updateProp() {
if(!selectedElement) return;
const get = (id) => document.getElementById(id).value;
const getNum = (id) => parseFloat(document.getElementById(id).value);
selectedElement.id = get('prop-id');
selectedElement.x = getNum('prop-x');
selectedElement.y = getNum('prop-y');
selectedElement.w = getNum('prop-w');
selectedElement.h = getNum('prop-h');
selectedElement.image = get('prop-image');
selectedElement.imgScale = getNum('prop-scale');
selectedElement.color = get('prop-color');
selectedElement.imgOffsetX = getNum('prop-imgx');
selectedElement.imgOffsetY = getNum('prop-imgy');
}
function setTool(t) {
currentTool = t;
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
const btns = document.querySelectorAll('.tool-btn');
for(let btn of btns) {
if(btn.innerText.toLowerCase().includes(t)) btn.classList.add('active');
}
selectedElement = null;
updateUI();
if (!selectedElement) return;
selectedElement.x = parseFloat(document.getElementById('prop-x').value);
selectedElement.y = parseFloat(document.getElementById('prop-y').value);
selectedElement.w = parseFloat(document.getElementById('prop-w').value);
selectedElement.h = parseFloat(document.getElementById('prop-h').value);
}
function deleteSelected() {
if(!selectedElement) return;
if (!selectedElement) return;
elements = elements.filter(e => e !== selectedElement);
selectedElement = null;
updateUI();
}
// --- SAVE / LOAD (DB) ---
async function refreshList() {
const sel = document.getElementById('chunk-select');
sel.innerHTML = "<option>Lade...</option>";
try {
const res = await fetch('/api/config');
loadedConfig = await res.json();
sel.innerHTML = "";
if(loadedConfig.chunks && loadedConfig.chunks.length > 0) {
loadedConfig.chunks.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.innerText = c.id + " (L=" + (c.totalWidth||'?') + ")";
sel.appendChild(opt);
});
} else { sel.innerHTML = "<option value=''>Keine Chunks</option>"; }
} catch(e) { console.error(e); }
}
// --- 6. SAVE / LOAD ---
function loadSelectedChunk() {
const id = document.getElementById('chunk-select').value;
if(!id || !loadedConfig) return;
if (!id) return;
const chunk = loadedConfig.chunks.find(c => c.id === id);
if(!chunk) return;
if(!confirm("Editor überschreiben?")) return;
if (!chunk) return alert("Chunk nicht gefunden");
if (!confirm("Editor leeren und Chunk laden?")) return;
document.getElementById('chunk-name').value = chunk.id;
document.getElementById('chunk-width').value = chunk.totalWidth || 2000;
elements = [];
const merge = (item, typeDefault) => ({
type: item.type || typeDefault,
id: item.id || 'obj',
x: item.x, y: item.y, w: item.w, h: item.h,
color: item.color || '#888',
image: item.image || '',
imgScale: item.imgScale || 1.0,
imgOffsetX: item.imgOffsetX || 0,
imgOffsetY: item.imgOffsetY || 0
});
const adder = (item, type) => {
elements.push({
id: item.id || 'plat', // Plattformen haben oft keine ID
type: type,
x: item.x, y: item.y, w: item.w, h: item.h,
_def: TOOL_DEFS[item.id] // Verknüpfung wiederherstellen
});
};
if(chunk.platforms) chunk.platforms.forEach(p => elements.push(merge(p, 'platform')));
if(chunk.obstacles) chunk.obstacles.forEach(o => elements.push(merge(o, 'obstacle')));
if (chunk.platforms) chunk.platforms.forEach(p => adder(p, 'platform'));
if (chunk.obstacles) chunk.obstacles.forEach(o => adder(o, o.type)); // Type aus Objekt oder Fallback
selectedElement = null;
updateUI();
alert(`Chunk '${id}' geladen!`);
}
async function saveChunk() {
const name = document.getElementById('chunk-name').value;
const width = parseInt(document.getElementById('chunk-width').value) || 2000;
for(let el of elements) {
if(!el.id) { alert("Fehler: ID fehlt!"); return; }
}
// Sauberes JSON bauen (ohne _def Referenzen)
const mapObj = (el) => ({
id: el.id, type: el.type, x: el.x, y: el.y, w: el.w, h: el.h, color: el.color,
image: el.image, imgScale: el.imgScale, imgOffsetX: el.imgOffsetX, imgOffsetY: el.imgOffsetY
id: el.id,
type: el.type,
x: el.x, y: el.y, w: el.w, h: el.h
});
const data = {
@@ -485,17 +461,14 @@
try {
const res = await fetch('/api/admin/chunks', {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
if(res.ok) { alert("Gespeichert!"); refreshList(); }
if (res.ok) alert("✅ Chunk gespeichert!");
else alert("Fehler beim Speichern");
} catch(e) { alert("Netzwerkfehler"); }
}
function exportJSON() {
// ... (Export Logic same as Save but to clipboard) ...
alert("Nutze Save (DB), Copy ist hier deaktiviert um Platz zu sparen.");
}
</script>
</body>
</html>