Private
Public Access
1
0
Files
it232Abschied/secure/editor.html
Sebastian Unterschütz 669c783a06
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
add music, better sync, particles
2025-11-29 23:37:57 +01:00

501 lines
21 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>Ultimate Chunk Editor (+Templates)</title>
<style>
body { background: #1a1a1a; color: #ddd; font-family: 'Segoe UI', monospace; display: flex; height: 100vh; margin: 0; overflow: hidden; }
#sidebar {
width: 340px; background: #2a2a2a; padding: 15px; border-right: 2px solid #444;
display: flex; flex-direction: column; gap: 10px; overflow-y: auto;
box-shadow: 2px 0 10px rgba(0,0,0,0.5);
}
#canvas-wrapper {
flex: 1; overflow: auto; position: relative; background: #333;
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; }
.tool-btn { padding: 8px; background: #444; border: 1px solid #555; color: white; cursor: pointer; text-align: left; border-radius: 4px; font-size: 12px; }
.tool-btn.active { background: #ffcc00; color: black; font-weight: bold; border-color: #ffcc00; }
.tool-btn:hover:not(.active) { background: #555; }
.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: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;}
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; }
</style>
</head>
<body>
<div id="sidebar">
<h3>🎨 VORLAGEN</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>
</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;">
<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: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>
<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>
</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>
<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">
<button class="action" onclick="saveChunk()" style="background:#4caf50;">💾 Speichern (DB)</button>
<button class="action" onclick="exportJSON()" style="background:#FF9800;">📋 JSON Copy</button>
</div>
</div>
</div>
<div id="canvas-wrapper">
<canvas id="editorCanvas" width="4000" height="500"></canvas>
</div>
<script>
const canvas = document.getElementById('editorCanvas');
const ctx = canvas.getContext('2d');
// State
let elements = [];
let selectedElement = null;
let currentTool = 'select';
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();
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 renderLoop() { draw(); requestAnimationFrame(renderLoop); }
function draw() {
ctx.clearRect(0,0,canvas.width, canvas.height);
// 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();
// 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);
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;
// Hitbox
ctx.fillStyle = el.color || '#888';
if (imgPath) ctx.fillStyle = "rgba(255, 0, 0, 0.2)";
ctx.fillRect(x, y, w, h);
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);
// 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;
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;
});
}
// --- INTERACTION ---
canvas.addEventListener('mousedown', e => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
if (currentTool === 'select') {
selectedElement = null;
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) {
selectedElement = el;
isDragging = true;
dragStart = { x: mx - el.x, y: my - el.y };
break;
}
}
} else {
const def = DEFAULTS[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
};
// --- 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();
}
}
updateUI();
});
canvas.addEventListener('mousemove', e => {
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;
updateUI();
}
});
window.addEventListener('mouseup', () => { isDragging = false; });
// --- 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);
} else {
props.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();
}
function deleteSelected() {
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); }
}
function loadSelectedChunk() {
const id = document.getElementById('chunk-select').value;
if(!id || !loadedConfig) return;
const chunk = loadedConfig.chunks.find(c => c.id === id);
if(!chunk) return;
if(!confirm("Editor überschreiben?")) 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
});
if(chunk.platforms) chunk.platforms.forEach(p => elements.push(merge(p, 'platform')));
if(chunk.obstacles) chunk.obstacles.forEach(o => elements.push(merge(o, 'obstacle')));
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; }
}
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
});
const data = {
id: name,
totalWidth: width,
platforms: elements.filter(e => e.type === 'platform').map(mapObj),
obstacles: elements.filter(e => e.type !== 'platform').map(mapObj)
};
try {
const res = await fetch('/api/admin/chunks', {
method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)
});
if(res.ok) { alert("Gespeichert!"); refreshList(); }
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>