All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m43s
449 lines
17 KiB
HTML
449 lines
17 KiB
HTML
<!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> |