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

449
secure/assets.html Normal file
View File

@@ -0,0 +1,449 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Asset Manager</title>
<style>
body { background: #1a1a1a; color: #ddd; font-family: 'Segoe UI', monospace; display: flex; height: 100vh; margin: 0; overflow: hidden; }
/* Sidebar: Liste */
#list-panel { width: 260px; background: #222; border-right: 1px solid #444; overflow-y: auto; display: flex; flex-direction: column; }
.list-item { padding: 10px; border-bottom: 1px solid #333; cursor: pointer; display: flex; align-items: center; gap: 10px; transition: background 0.2s; }
.list-item:hover { background: #333; }
.list-item.active { background: #2196F3; color: white; border-left: 4px solid #fff; }
.list-thumb { width: 32px; height: 32px; object-fit: contain; background: rgba(0,0,0,0.3); border-radius: 4px; }
/* Main: Editor */
#editor-panel { flex: 1; padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
/* Preview Canvas Box */
.preview-box {
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-color: #333;
border: 2px solid #555; height: 300px; position: relative;
border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.5);
}
#preview-canvas { width: 100%; height: 100%; display: block; }
/* Formular Gruppen */
.group { background: #2a2a2a; padding: 12px; border: 1px solid #444; border-radius: 6px; }
.group-title { font-size: 11px; color: #2196F3; font-weight: bold; margin-bottom: 8px; text-transform: uppercase; border-bottom: 1px solid #444; padding-bottom: 4px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
label { font-size: 10px; color: #aaa; text-transform: uppercase; display: block; margin-bottom: 3px; }
input, select {
width: 100%; background: #151515; border: 1px solid #555; color: white;
padding: 8px; box-sizing: border-box; border-radius: 4px; font-family: monospace;
}
input:focus { border-color: #ffcc00; outline: none; }
/* Checkbox Style */
.checkbox-label { display: flex; align-items: center; gap: 8px; cursor: pointer; background: #111; padding: 8px; border-radius: 4px; border: 1px solid #444; }
.checkbox-label input { width: auto; margin: 0; }
.checkbox-label:hover { background: #333; }
button { padding: 10px; cursor: pointer; border: none; font-weight: bold; border-radius: 4px; transition: all 0.2s; }
button:hover { filter: brightness(1.1); transform: translateY(-1px); }
.btn-save { background: #4caf50; color: white; width: 100%; margin-top: 10px; font-size: 14px; padding: 12px; }
.btn-upload { background: #FF9800; color: white; white-space: nowrap;}
.btn-add { background: #2196F3; color: white; width: 100%; margin-top: auto; border-radius: 0; padding: 15px; }
.btn-delete { background: #f44336; color: white; margin-top: 10px; width: 100%; }
</style>
</head>
<body>
<div id="list-panel">
<h3 style="padding:15px; margin:0; background:#2a2a2a; border-bottom:1px solid #444; color:#ffcc00;">📦 ASSET MANAGER</h3>
<div id="asset-list">Lade Daten...</div>
<button class="btn-add" onclick="createNew()">+ NEUES OBJEKT</button>
</div>
<div id="editor-panel" style="display:none;">
<div class="preview-box">
<canvas id="preview-canvas"></canvas>
<div style="position:absolute; bottom:5px; right:5px; font-size:10px; color:white; opacity:0.5; pointer-events:none;">
Grün: Spieler | Rot: Hitbox | Blau: Textur
</div>
</div>
<div style="display: flex; gap: 20px;">
<div style="flex:1; display:flex; flex-direction:column; gap:15px;">
<div class="group">
<div class="group-title">Basis</div>
<div class="form-grid">
<div>
<label>ID (Einzigartig)</label>
<input type="text" id="inp-id" oninput="saveLocalState()">
</div>
<div>
<label>Typ</label>
<select id="inp-type" oninput="saveLocalState()">
<option value="obstacle">Hindernis</option>
<option value="teacher">Lehrer (Tödlich)</option>
<option value="coin">Coin</option>
<option value="powerup">PowerUp</option>
</select>
</div>
</div>
<div style="margin-top:10px;">
<label>Sprechblasen (Komma getrennt)</label>
<input type="text" id="inp-speech" placeholder="Halt!, Hier geblieben!" oninput="saveLocalState()">
</div>
<div style="margin-top:10px;">
<label class="checkbox-label">
<input type="checkbox" id="inp-norandom" onchange="saveLocalState()">
<span style="color:#ff8a80;">⛔ Nicht Random spawnen</span>
</label>
<small style="color:#666; margin-left:5px;">Nur manuell im Editor platzierbar</small>
</div>
</div>
<div class="group">
<div class="group-title">Textur & Upload</div>
<label>Dateiname (/static/assets/)</label>
<div style="display:flex; gap:5px;">
<input type="text" id="inp-image" placeholder="bild.png" oninput="updateImagePreview()">
<input type="file" id="file-upload" style="display:none" onchange="uploadFile()">
<button class="btn-upload" onclick="document.getElementById('file-upload').click()">📂 Upload</button>
</div>
<div class="form-grid" style="margin-top:10px;">
<div><label>Fallback Farbe</label><input type="color" id="inp-color" oninput="draw()" style="height:35px; padding:2px;"></div>
</div>
</div>
</div>
<div style="flex:1; display:flex; flex-direction:column; gap:15px;">
<div class="group" style="border-color:#ff4444;">
<div class="group-title" style="color:#ff4444;">🔴 Hitbox (Physik)</div>
<div class="form-grid">
<div><label>Breite</label><input type="number" id="inp-w" oninput="draw()"></div>
<div><label>Höhe</label><input type="number" id="inp-h" oninput="draw()"></div>
</div>
<div style="margin-top:10px;">
<label>Y-Offset (Schweben)</label>
<input type="number" id="inp-yoff" oninput="draw()">
<small style="color:#666">Positiv = Höher</small>
</div>
</div>
<div class="group" style="border-color:#2196F3;">
<div class="group-title" style="color:#2196F3;">🖼️ Optik (Textur)</div>
<div class="form-grid">
<div><label>Scale</label><input type="number" step="0.1" id="inp-scale" value="1.0" oninput="draw()"></div>
<div></div>
<div><label>Img Offset X</label><input type="number" id="inp-offx" value="0" oninput="draw()"></div>
<div><label>Img Offset Y</label><input type="number" id="inp-offy" value="0" oninput="draw()"></div>
</div>
</div>
</div>
</div>
<div style="margin-top:auto; padding-top:20px; border-top:1px solid #444;">
<button class="btn-save" onclick="saveAll()">💾 CONFIG SPEICHERN & SERVER NEUSTARTEN</button>
<button class="btn-delete" onclick="deleteCurrent()">🗑 Dieses Asset löschen</button>
</div>
</div>
<script>
// Globale Variablen
let config = { obstacles: [], backgrounds: [] };
let currentIdx = -1;
// Canvas Context
const canvas = document.getElementById('preview-canvas');
const ctx = canvas.getContext('2d', { alpha: false });
let imgCache = new Image();
let dpr = 1;
// --- INIT ---
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
loadData();
function resizeCanvas() {
const container = canvas.parentElement;
const displayWidth = container.clientWidth;
const displayHeight = container.clientHeight;
dpr = window.devicePixelRatio || 1;
canvas.width = displayWidth * dpr;
canvas.height = displayHeight * dpr;
canvas.style.width = `${displayWidth}px`;
canvas.style.height = `${displayHeight}px`;
ctx.scale(dpr, dpr);
draw();
}
// 1. DATEN LADEN
async function loadData() {
try {
const res = await fetch('/api/config');
if(!res.ok) throw new Error("Server antwortet nicht");
const data = await res.json();
// Safety Check & Lowercase Enforcing
config = {
obstacles: data.obstacles || data.Obstacles || [],
backgrounds: data.backgrounds || data.Backgrounds || []
};
renderList();
} catch (e) {
console.error(e);
document.getElementById('asset-list').innerHTML = "<div style='padding:10px; color:red'>Fehler beim Laden!</div>";
}
}
// 2. LISTE RENDERN
function renderList() {
const list = document.getElementById('asset-list');
list.innerHTML = "";
if (config.obstacles.length === 0) {
list.innerHTML = "<div style='padding:15px; color:#666; text-align:center;'>Keine Assets.<br>Erstelle eins!</div>";
return;
}
config.obstacles.forEach((obs, idx) => {
const div = document.createElement('div');
div.className = `list-item ${idx === currentIdx ? 'active' : ''}`;
const imgPath = obs.image ? `../assets/${obs.image}` : '';
div.innerHTML = `
<img src="${imgPath}" class="list-thumb" onerror="this.style.opacity=0">
<div>
<b style="font-size:12px;">${obs.id || 'Ohne ID'}</b><br>
<small style="color:#aaa;">${obs.type}</small>
</div>
`;
div.onclick = () => selectItem(idx);
list.appendChild(div);
});
}
// 3. ITEM AUSWÄHLEN
function selectItem(idx) {
currentIdx = idx;
const obs = config.obstacles[idx];
if (!obs) return;
document.getElementById('editor-panel').style.display = 'flex';
const set = (id, val) => document.getElementById(id).value = val;
set('inp-id', obs.id || '');
set('inp-type', obs.type || 'obstacle');
set('inp-image', obs.image || '');
set('inp-w', obs.width || 30);
set('inp-h', obs.height || 30);
set('inp-yoff', obs.yOffset || 0);
set('inp-scale', obs.imgScale || 1.0);
set('inp-offx', obs.imgOffsetX || 0);
set('inp-offy', obs.imgOffsetY || 0);
set('inp-color', obs.color || '#ff0000');
const speech = obs.speechLines ? obs.speechLines.join(', ') : '';
set('inp-speech', speech);
// Checkbox: NoRandomSpawn
document.getElementById('inp-norandom').checked = obs.noRandomSpawn === true;
updateImagePreview();
renderList();
}
// 4. NEUES ITEM
function createNew() {
const newObj = {
id: 'neu_' + Math.floor(Math.random()*1000),
type: 'obstacle',
width: 30, height: 30,
color: '#00ff00',
imgScale: 1.0, imgOffsetX: 0, imgOffsetY: 0,
noRandomSpawn: false
};
config.obstacles.push(newObj);
selectItem(config.obstacles.length - 1);
const list = document.getElementById('list-panel');
setTimeout(() => list.scrollTop = list.scrollHeight, 50);
}
// 5. LÖSCHEN
function deleteCurrent() {
if (currentIdx === -1) return;
if (!confirm("Dieses Asset wirklich löschen?")) return;
config.obstacles.splice(currentIdx, 1);
document.getElementById('editor-panel').style.display = 'none';
currentIdx = -1;
renderList();
}
// 6. UPLOAD
async function uploadFile() {
const input = document.getElementById('file-upload');
if (input.files.length === 0) return;
const formData = new FormData();
formData.append("file", input.files[0]);
try {
const res = await fetch('/api/admin/upload', {
method: 'POST',
body: formData
});
if (!res.ok) throw new Error("Upload Error " + res.status);
const data = await res.json();
document.getElementById('inp-image').value = data.filename;
saveLocalState();
updateImagePreview();
} catch(e) {
alert("Upload fehlgeschlagen: " + e.message);
}
}
// 7. SPEICHERN
async function saveAll() {
saveLocalState();
try {
const res = await fetch('/api/admin/save-config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(config)
});
if (res.ok) {
alert("✅ Gespeichert! Server geupdatet.");
renderList();
} else {
alert("❌ Fehler beim Speichern");
}
} catch(e) {
alert("Netzwerkfehler: " + e);
}
}
// --- HELPER ---
function saveLocalState() {
if (currentIdx === -1) return;
const obs = config.obstacles[currentIdx];
const get = (id) => document.getElementById(id).value;
const getNum = (id) => parseFloat(document.getElementById(id).value) || 0;
obs.id = get('inp-id');
obs.type = get('inp-type');
obs.image = get('inp-image');
obs.width = getNum('inp-w');
obs.height = getNum('inp-h');
obs.yOffset = getNum('inp-yoff');
obs.imgScale = getNum('inp-scale');
obs.imgOffsetX = getNum('inp-offx');
obs.imgOffsetY = getNum('inp-offy');
obs.color = get('inp-color');
// Random Flag
obs.noRandomSpawn = document.getElementById('inp-norandom').checked;
const s = get('inp-speech');
obs.speechLines = s ? s.split(',').map(x => x.trim()).filter(x => x) : [];
obs.canTalk = obs.speechLines.length > 0;
draw();
}
function updateImagePreview() {
const path = document.getElementById('inp-image').value;
if (path) {
imgCache.src = "../assets/" + path;
} else {
imgCache.src = "";
}
imgCache.onload = () => { draw(); renderList(); };
imgCache.onerror = draw;
}
// --- DRAWING ---
function draw() {
const w = canvas.width / dpr;
const h = canvas.height / dpr;
const groundY = h * 0.8;
const centerX = w / 2;
ctx.clearRect(0,0,w,h);
// Boden
ctx.fillStyle = "#222"; ctx.fillRect(0, groundY, w, 2);
ctx.fillStyle = "#444"; ctx.font="10px monospace"; ctx.fillText(`GROUND Y=${Math.floor(groundY)}`, 5, groundY+12);
// Player Dummy
const pX = centerX - 100;
ctx.fillStyle = "#33cc33"; ctx.fillRect(pX, groundY-50, 30, 50);
ctx.fillStyle = "white"; ctx.fillText("Player", pX, groundY-55);
if (currentIdx === -1) return;
// Werte lesen
const width = parseFloat(document.getElementById('inp-w').value) || 30;
const height = 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 offX = parseFloat(document.getElementById('inp-offx').value) || 0;
const offY = parseFloat(document.getElementById('inp-offy').value) || 0;
const color = document.getElementById('inp-color').value;
// Objekt Position
const oX = centerX + 50 - (width / 2);
const oY = groundY - height - yOff;
// 1. HITBOX
ctx.fillStyle = color;
ctx.globalAlpha = 0.3;
ctx.fillRect(oX, oY, width, height);
ctx.globalAlpha = 1.0;
ctx.strokeStyle = "red"; ctx.lineWidth = 2;
ctx.strokeRect(oX, oY, width, height);
ctx.fillStyle = "red";
ctx.fillText(`Hitbox: ${width}x${height}`, oX, oY-5);
// 2. TEXTUR
if (imgCache.complete && imgCache.naturalHeight !== 0) {
const dW = width * scale;
const dH = height * scale;
// Zentriert & Unten Bündig + Offset
const bX = oX + (width - dW)/2 + offX;
const bY = oY + (height - dH) + offY;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(imgCache, bX, bY, dW, dH);
ctx.strokeStyle = "cyan"; ctx.lineWidth = 1;
ctx.strokeRect(bX, bY, dW, dH);
}
// Mitte-Linie
ctx.strokeStyle = "#444"; ctx.lineWidth = 1; ctx.setLineDash([5, 5]);
ctx.beginPath(); ctx.moveTo(centerX, 0); ctx.lineTo(centerX, h); ctx.stroke(); ctx.setLineDash([]);
}
</script>
</body>
</html>