add music, better sync, particles
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m18s
This commit is contained in:
501
secure/editor.html
Normal file
501
secure/editor.html
Normal file
@@ -0,0 +1,501 @@
|
||||
<!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>
|
||||
355
secure/obstacle_editor.html
Normal file
355
secure/obstacle_editor.html
Normal file
@@ -0,0 +1,355 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Obstacle Tuner (Realtime)</title>
|
||||
<style>
|
||||
body { background: #1a1a1a; color: #ddd; font-family: 'Segoe UI', monospace; display: flex; height: 100vh; margin: 0; overflow: hidden; }
|
||||
|
||||
/* Sidebar */
|
||||
#controls {
|
||||
width: 360px; background: #2a2a2a; padding: 15px; border-right: 2px solid #444;
|
||||
display: flex; flex-direction: column; gap: 10px; overflow-y: auto;
|
||||
box-shadow: 5px 0 15px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* Canvas Area */
|
||||
#preview {
|
||||
flex: 1; background-color: #333;
|
||||
/* Schachbrettmuster */
|
||||
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;
|
||||
display: flex; align-items: center; justify-content: center; position: relative;
|
||||
}
|
||||
|
||||
canvas { border: 2px solid #555; background: rgba(0,0,0,0.3); box-shadow: 0 0 20px rgba(0,0,0,0.8); cursor: grab; }
|
||||
canvas:active { cursor: grabbing; }
|
||||
|
||||
/* Controls */
|
||||
.group { background: #333; padding: 8px; border-radius: 6px; border: 1px solid #444; }
|
||||
.group-title { font-size: 10px; color: #ffcc00; font-weight: bold; margin-bottom: 5px; text-transform: uppercase; border-bottom: 1px solid #444; padding-bottom: 2px; display:flex; justify-content:space-between;}
|
||||
|
||||
label { display: block; color: #888; font-size: 10px; margin-bottom: 1px; }
|
||||
input, select, textarea {
|
||||
width: 100%; box-sizing: border-box; background: #1a1a1a; border: 1px solid #555;
|
||||
color: #fff; padding: 4px 6px; border-radius: 4px; font-family: monospace; font-size: 12px;
|
||||
}
|
||||
input:focus { outline: none; border-color: #ffcc00; }
|
||||
input[type=range] { padding: 0; margin: 5px 0; cursor: pointer; }
|
||||
|
||||
.row { display: flex; gap: 8px; align-items: center; }
|
||||
h2 { margin: 0 0 10px 0; color: #ffcc00; border-bottom: 1px solid #555; padding-bottom: 10px; font-size: 18px; }
|
||||
#output { height: 60px; font-size: 10px; color: #aaddff; resize: vertical; margin-top: auto;}
|
||||
|
||||
button { background: #4caf50; color: white; border: none; padding: 8px; font-weight: bold; cursor: pointer; border-radius: 4px; width: 100%; font-size: 12px; }
|
||||
button:hover { background: #45a049; }
|
||||
.btn-copy { background: #2196F3; margin-top: 5px; }
|
||||
|
||||
/* Highlight wenn Dragging */
|
||||
.drag-active { border: 1px solid #2196F3 !important; background: #2a3a4a !important; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="controls">
|
||||
<h2>🛠 OBSTACLE TUNER</h2>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-title">Basis</div>
|
||||
<div class="row">
|
||||
<div style="flex:2"><label>ID</label><input type="text" id="inp-id" value="new_item" oninput="update()"></div>
|
||||
<div style="flex:1"><label>Type</label>
|
||||
<select id="inp-type" oninput="update()">
|
||||
<option value="obstacle">Obstacle</option>
|
||||
<option value="teacher">Teacher</option>
|
||||
<option value="coin">Coin</option>
|
||||
<option value="powerup">PowerUp</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<label style="margin-top:5px">Bild Datei (../../assets/)</label>
|
||||
<input type="text" id="inp-image" value="teacher1.png" oninput="loadImage()">
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<div class="group-title">🔴 Hitbox (Physik)</div>
|
||||
<div class="row">
|
||||
<div style="flex:1"><label>Breite</label><input type="number" id="inp-w" value="30" oninput="update()"></div>
|
||||
<div style="flex:1"><label>Höhe</label><input type="number" id="inp-h" value="60" oninput="update()"></div>
|
||||
</div>
|
||||
<input type="range" min="10" max="200" value="30" id="slider-w" oninput="syncSlider('w')">
|
||||
<input type="range" min="10" max="200" value="60" id="slider-h" oninput="syncSlider('h')">
|
||||
|
||||
<label>Y-Offset (Schweben)</label>
|
||||
<div class="row">
|
||||
<input type="number" id="inp-yoff" value="0" oninput="update()">
|
||||
<input type="range" min="-50" max="100" value="0" step="5" id="slider-yoff" oninput="syncSlider('yoff')">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group" style="border-color: #2196F3;" id="grp-visuals">
|
||||
<div class="group-title" style="color:#2196F3">
|
||||
<span>🖼️ Optik (Textur)</span>
|
||||
<small style="font-weight:normal; font-size:9px; color:#aaa;">Drag Canvas to Move</small>
|
||||
</div>
|
||||
|
||||
<label>Scale (Größe)</label>
|
||||
<div class="row">
|
||||
<input type="number" id="inp-scale" value="1.0" step="0.1" oninput="syncInput('scale')">
|
||||
<input type="range" min="0.5" max="3.0" step="0.1" value="1.0" id="slider-scale" oninput="syncSlider('scale')">
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top:5px;">
|
||||
<div style="flex:1"><label>Offset X</label><input type="number" id="inp-imgx" value="0" oninput="update()"></div>
|
||||
<div style="flex:1"><label>Offset Y</label><input type="number" id="inp-imgy" value="0" oninput="update()"></div>
|
||||
</div>
|
||||
<button onclick="resetVisuals()" style="background:#555; margin-top:5px; padding:4px; font-size:10px;">Reset Optik</button>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<label>Fallback Farbe</label>
|
||||
<input type="color" id="inp-color" value="#ff0000" style="height:25px; padding:0;" oninput="update()">
|
||||
</div>
|
||||
|
||||
<textarea id="output" readonly onclick="this.select()"></textarea>
|
||||
<button class="btn-copy" onclick="copyToClipboard()">📋 GO STRING KOPIEREN</button>
|
||||
</div>
|
||||
|
||||
<div id="preview">
|
||||
<canvas id="canvas" width="600" height="400"></canvas>
|
||||
<div style="position:absolute; bottom: 10px; right: 10px; color:white; font-size:10px; text-align:right; pointer-events:none;">
|
||||
<span style="color:#2196F3; font-weight:bold;">Maus Drag: Bild verschieben</span><br>
|
||||
Grün = Spieler (Referenz)<br>Rot = Hitbox
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Konstanten
|
||||
const GROUND_Y = 300;
|
||||
const PLAYER = { w: 30, h: 50, color: '#33cc33' };
|
||||
|
||||
// Bild Cache
|
||||
let imgObj = new Image();
|
||||
let imgLoaded = false;
|
||||
|
||||
// Dragging Status
|
||||
let isDragging = false;
|
||||
let dragStart = {x:0, y:0};
|
||||
let startOffset = {x:0, y:0};
|
||||
|
||||
// --- INIT ---
|
||||
loadImage(); // Lädt Bild und startet Draw Loop
|
||||
|
||||
// --- EVENTS & LOGIC ---
|
||||
|
||||
// 1. Slider <-> Input Sync (Bi-Direktional)
|
||||
function syncSlider(id) {
|
||||
document.getElementById('inp-'+id).value = document.getElementById('slider-'+id).value;
|
||||
update();
|
||||
}
|
||||
function syncInput(id) {
|
||||
document.getElementById('slider-'+id).value = document.getElementById('inp-'+id).value;
|
||||
update();
|
||||
}
|
||||
|
||||
// 2. Reset Button
|
||||
function resetVisuals() {
|
||||
document.getElementById('inp-scale').value = 1.0;
|
||||
document.getElementById('slider-scale').value = 1.0;
|
||||
document.getElementById('inp-imgx').value = 0;
|
||||
document.getElementById('inp-imgy').value = 0;
|
||||
update();
|
||||
}
|
||||
|
||||
// 3. Bild Laden (Nur wenn Pfad sich ändert!)
|
||||
function loadImage() {
|
||||
const path = document.getElementById('inp-image').value;
|
||||
imgLoaded = false;
|
||||
imgObj = new Image();
|
||||
// Pfad-Logik für Local vs Server
|
||||
imgObj.src = "../../assets/" + path;
|
||||
|
||||
imgObj.onload = () => {
|
||||
imgLoaded = true;
|
||||
draw(); // Sofort neu zeichnen
|
||||
};
|
||||
imgObj.onerror = () => {
|
||||
// Fallback Pfad probieren
|
||||
imgObj.src = "../../assets/" + path;
|
||||
};
|
||||
draw();
|
||||
}
|
||||
|
||||
// 4. Zentrales Update (Alles außer Bildpfad)
|
||||
function update() {
|
||||
// Hier wird NICHT das Bild neu geladen, nur Parameter gelesen
|
||||
draw();
|
||||
generateString();
|
||||
}
|
||||
|
||||
// --- DRAG & DROP LOGIK ---
|
||||
canvas.addEventListener('mousedown', e => {
|
||||
isDragging = true;
|
||||
dragStart = { x: e.clientX, y: e.clientY };
|
||||
|
||||
// Aktuelle Werte holen
|
||||
startOffset = {
|
||||
x: parseFloat(document.getElementById('inp-imgx').value) || 0,
|
||||
y: parseFloat(document.getElementById('inp-imgy').value) || 0
|
||||
};
|
||||
|
||||
document.getElementById('grp-visuals').classList.add('drag-active');
|
||||
canvas.style.cursor = "grabbing";
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', e => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const dx = e.clientX - dragStart.x;
|
||||
const dy = e.clientY - dragStart.y;
|
||||
|
||||
// Neue Werte berechnen (Integer reichen meist für Pixel Art)
|
||||
const newX = Math.round(startOffset.x + dx);
|
||||
const newY = Math.round(startOffset.y + dy);
|
||||
|
||||
// Inputs updaten (ohne draw aufzurufen, das machen wir direkt)
|
||||
document.getElementById('inp-imgx').value = newX;
|
||||
document.getElementById('inp-imgy').value = newY;
|
||||
|
||||
update(); // Löst Draw & String Gen aus
|
||||
});
|
||||
|
||||
window.addEventListener('mouseup', () => {
|
||||
if(isDragging) {
|
||||
isDragging = false;
|
||||
document.getElementById('grp-visuals').classList.remove('drag-active');
|
||||
canvas.style.cursor = "grab";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- RENDERING ---
|
||||
function draw() {
|
||||
// Canvas leeren
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// 1. BODEN
|
||||
ctx.fillStyle = "#222";
|
||||
ctx.fillRect(0, GROUND_Y, canvas.width, 2);
|
||||
|
||||
// 2. SPIELER (DUMMY)
|
||||
const pX = 200;
|
||||
const pY = GROUND_Y - PLAYER.h;
|
||||
ctx.fillStyle = PLAYER.color;
|
||||
ctx.fillRect(pX, pY, PLAYER.w, PLAYER.h);
|
||||
ctx.fillStyle = "white"; ctx.font = "10px sans-serif"; ctx.fillText("PLAYER", pX, pY - 5);
|
||||
|
||||
// 3. PARAMETER LESEN
|
||||
const w = parseFloat(document.getElementById('inp-w').value) || 30;
|
||||
const h = parseFloat(document.getElementById('inp-h').value) || 30;
|
||||
const yOff = parseFloat(document.getElementById('inp-yoff').value) || 0;
|
||||
const scale = parseFloat(document.getElementById('inp-scale').value) || 1.0;
|
||||
const imgX = parseFloat(document.getElementById('inp-imgx').value) || 0;
|
||||
const imgY = parseFloat(document.getElementById('inp-imgy').value) || 0;
|
||||
const color = document.getElementById('inp-color').value;
|
||||
const id = document.getElementById('inp-id').value;
|
||||
|
||||
// Position des Objekts (300px = Rechts vom Spieler)
|
||||
const oX = 300;
|
||||
const oY = GROUND_Y - h - yOff;
|
||||
|
||||
// A. HITBOX ZEICHNEN (Unter dem Bild)
|
||||
ctx.fillStyle = "rgba(255, 0, 0, 0.25)"; // Halbtransparent
|
||||
ctx.fillRect(oX, oY, w, h);
|
||||
ctx.strokeStyle = "#ff0000"; ctx.lineWidth = 2; ctx.setLineDash([]);
|
||||
ctx.strokeRect(oX, oY, w, h);
|
||||
|
||||
// Label
|
||||
ctx.fillStyle = "#ff5555";
|
||||
ctx.fillText(`HITBOX ${w}x${h}`, oX, oY - 5);
|
||||
|
||||
// B. BILD ZEICHNEN
|
||||
if (imgLoaded) {
|
||||
const drawW = w * scale;
|
||||
const drawH = h * scale;
|
||||
|
||||
// Logik wie im Spiel:
|
||||
// 1. Zentrieren auf Hitbox
|
||||
const centerX = oX + (w - drawW) / 2;
|
||||
// 2. Unten Bündig
|
||||
const bottomY = oY + (h - drawH);
|
||||
|
||||
// 3. Offsets anwenden
|
||||
const finalX = centerX + imgX;
|
||||
const finalY = bottomY + imgY;
|
||||
|
||||
// Zeichnen
|
||||
ctx.drawImage(imgObj, finalX, finalY, drawW, drawH);
|
||||
|
||||
// Blauer Rahmen um Textur
|
||||
ctx.strokeStyle = "#2196F3"; ctx.lineWidth = 1; ctx.setLineDash([4, 2]);
|
||||
ctx.strokeRect(finalX, finalY, drawW, drawH);
|
||||
|
||||
// Verbindungslinie (Center Hitbox -> Center Bild)
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(oX + w/2, oY + h/2);
|
||||
ctx.lineTo(finalX + drawW/2, finalY + drawH/2);
|
||||
ctx.strokeStyle = "rgba(33, 150, 243, 0.4)";
|
||||
ctx.stroke();
|
||||
|
||||
} else {
|
||||
// Fallback Grafik
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(oX, oY, w, h);
|
||||
ctx.fillStyle = "white";
|
||||
ctx.fillText("IMG FEHLT", oX+2, oY + h/2);
|
||||
}
|
||||
}
|
||||
|
||||
// --- EXPORT ---
|
||||
function generateString() {
|
||||
const id = document.getElementById('inp-id').value;
|
||||
const type = document.getElementById('inp-type').value;
|
||||
const w = document.getElementById('inp-w').value;
|
||||
const h = document.getElementById('inp-h').value;
|
||||
const color = document.getElementById('inp-color').value;
|
||||
const img = document.getElementById('inp-image').value;
|
||||
const yOff = parseFloat(document.getElementById('inp-yoff').value);
|
||||
const scale = parseFloat(document.getElementById('inp-scale').value);
|
||||
const imgX = parseFloat(document.getElementById('inp-imgx').value);
|
||||
const imgY = parseFloat(document.getElementById('inp-imgy').value);
|
||||
|
||||
// String bauen
|
||||
let str = `{ID: "${id}", Type: "${type}", Width: ${w}, Height: ${h}, Color: "${color}", Image: "${img}"`;
|
||||
|
||||
if (yOff !== 0) str += `, YOffset: ${yOff}`;
|
||||
if (scale !== 1.0) str += `, ImgScale: ${scale}`;
|
||||
if (imgX !== 0) str += `, ImgOffsetX: ${imgX}`;
|
||||
if (imgY !== 0) str += `, ImgOffsetY: ${imgY}`;
|
||||
|
||||
str += `}, // ${id}`;
|
||||
|
||||
document.getElementById('output').value = str;
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
const copyText = document.getElementById("output");
|
||||
copyText.select();
|
||||
navigator.clipboard.writeText(copyText.value);
|
||||
|
||||
const btn = document.querySelector('.btn-copy');
|
||||
const oldText = btn.innerText;
|
||||
btn.innerText = "✅ KOPIERT!";
|
||||
btn.style.background = "#2e7d32";
|
||||
setTimeout(() => {
|
||||
btn.innerText = oldText;
|
||||
btn.style.background = "#2196F3";
|
||||
}, 1000);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user