fix
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m43s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m43s
This commit is contained in:
53
config.go
53
config.go
@@ -36,32 +36,45 @@ func getEnv(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// config.go
|
||||
|
||||
func initGameConfig() {
|
||||
defaultConfig = GameConfig{
|
||||
Obstacles: []ObstacleDef{
|
||||
// --- HINDERNISSE ---
|
||||
{ID: "desk", Type: "obstacle", Width: 50, Height: 65, Color: "#ff0000", Image: "desk.png", YOffset: -19, ImgScale: 1.3, ImgOffsetX: 1, ImgOffsetY: 3}, // desk
|
||||
{ID: "teacher", Type: "teacher", Width: 30, Height: 60, Color: "#000080", Image: "teacher1.png"},
|
||||
{ID: "k-m", Type: "teacher", Width: 45, Height: 80, Color: "#ff0000", Image: "k-m.png", YOffset: 5, ImgScale: 1.2, ImgOffsetX: -1, ImgOffsetY: 8}, // k-m
|
||||
{ID: "w-l", Type: "teacher", Width: 50, Height: 70, Color: "#ff0000", Image: "w-l.png", ImgScale: 1.1, ImgOffsetX: 1, ImgOffsetY: 3, CanTalk: true, SpeechLines: []string{"Halt!", "Handy weg!"}}, // w-l
|
||||
{ID: "trashcan", Type: "obstacle", Width: 25, Height: 35, Color: "#555", Image: "trash1.png"},
|
||||
{ID: "eraser1", Type: "obstacle", Width: 56, Height: 37, Color: "#ff0000", Image: "eraser.png", YOffset: 35, ImgScale: 1.6, ImgOffsetY: 9}, // eraser1
|
||||
{ID: "principal", Type: "teacher", Width: 40, Height: 70, Color: "#000", Image: "principal1.png", CanTalk: true, SpeechLines: []string{"EXMATRIKULATION!"}},
|
||||
// 1. Versuche aus Redis zu laden
|
||||
val, err := rdb.Get(ctx, "config:gamedata").Result()
|
||||
|
||||
// --- COINS ---
|
||||
{ID: "coin0", Type: "coin", Width: 40, Height: 73, Color: "#ff0000", Image: "coin.png", ImgScale: 1.1, ImgOffsetY: 1},
|
||||
{ID: "coin1", Type: "coin", Width: 40, Height: 73, Color: "#ff0000", Image: "coin.png", YOffset: 60, ImgScale: 1.1, ImgOffsetY: 1},
|
||||
if err == nil && val != "" {
|
||||
// Redis hat Daten -> Nutzen!
|
||||
json.Unmarshal([]byte(val), &defaultConfig)
|
||||
log.Println("💾 Config aus Redis geladen.")
|
||||
} else {
|
||||
// Redis ist leer -> Hardcoded Defaults laden (Bootstrap)
|
||||
log.Println("⚠️ Keine Config in Redis -> Nutze Hardcoded Defaults.")
|
||||
defaultConfig = GameConfig{
|
||||
Obstacles: []ObstacleDef{
|
||||
// --- HINDERNISSE ---
|
||||
{ID: "desk", Type: "obstacle", Width: 50, Height: 65, Color: "#ff0000", Image: "desk.png", YOffset: -19, ImgScale: 1.3, ImgOffsetX: 1, ImgOffsetY: 3}, // desk
|
||||
{ID: "teacher", Type: "teacher", Width: 30, Height: 60, Color: "#000080", Image: "teacher1.png"},
|
||||
{ID: "k-m", Type: "teacher", Width: 45, Height: 80, Color: "#ff0000", Image: "k-m.png", YOffset: 5, ImgScale: 1.2, ImgOffsetX: -1, ImgOffsetY: 8}, // k-m
|
||||
{ID: "w-l", Type: "teacher", Width: 50, Height: 70, Color: "#ff0000", Image: "w-l.png", ImgScale: 1.1, ImgOffsetX: 1, ImgOffsetY: 3, CanTalk: true, SpeechLines: []string{"Halt!", "Handy weg!"}}, // w-l
|
||||
{ID: "trashcan", Type: "obstacle", Width: 25, Height: 35, Color: "#555", Image: "trash1.png"},
|
||||
{ID: "eraser1", Type: "obstacle", Width: 56, Height: 37, Color: "#ff0000", Image: "eraser.png", YOffset: 35, ImgScale: 1.6, ImgOffsetY: 9}, // eraser1
|
||||
{ID: "principal", Type: "teacher", Width: 40, Height: 70, Color: "#000", Image: "principal1.png", CanTalk: true, SpeechLines: []string{"EXMATRIKULATION!"}},
|
||||
|
||||
// --- POWERUPS ---
|
||||
{ID: "p_god", Type: "powerup", Width: 30, Height: 30, Color: "cyan", Image: "powerup_god1.png", YOffset: 20.0}, // Godmode
|
||||
{ID: "p_bat", Type: "powerup", Width: 30, Height: 30, Color: "red", Image: "powerup_bat1.png", YOffset: 20.0}, // Schläger
|
||||
{ID: "p_boot", Type: "powerup", Width: 30, Height: 30, Color: "lime", Image: "powerup_boot1.png", YOffset: 20.0}, // Boots
|
||||
},
|
||||
// --- COINS ---
|
||||
{ID: "coin0", Type: "coin", Width: 40, Height: 73, Color: "#ff0000", Image: "coin.png", ImgScale: 1.1, ImgOffsetY: 1},
|
||||
{ID: "coin1", Type: "coin", Width: 40, Height: 73, Color: "#ff0000", Image: "coin.png", YOffset: 60, ImgScale: 1.1, ImgOffsetY: 1},
|
||||
|
||||
Backgrounds: []string{"school-background.jpg", "gym-background.jpg", "school2-background.jpg"},
|
||||
// --- POWERUPS ---
|
||||
{ID: "p_god", Type: "powerup", Width: 30, Height: 30, Color: "cyan", Image: "powerup_god1.png", YOffset: 20.0}, // Godmode
|
||||
{ID: "p_bat", Type: "powerup", Width: 30, Height: 30, Color: "red", Image: "powerup_bat1.png", YOffset: 20.0}, // Schläger
|
||||
{ID: "p_boot", Type: "powerup", Width: 30, Height: 30, Color: "lime", Image: "powerup_boot1.png", YOffset: 20.0}, // Boots
|
||||
},
|
||||
|
||||
Backgrounds: []string{"school-background.jpg", "gym-background.jpg", "school2-background.jpg"},
|
||||
}
|
||||
}
|
||||
log.Println("✅ Config mit Powerups geladen")
|
||||
|
||||
// Chunks laden (bleibt wie vorher)
|
||||
defaultConfig.Chunks = loadChunksFromRedis()
|
||||
}
|
||||
|
||||
|
||||
67
handlers.go
67
handlers.go
@@ -3,9 +3,12 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -344,3 +347,67 @@ func handleAdminBadwords(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func handleAdminUpload(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Nur POST erlaubt", 405)
|
||||
return
|
||||
}
|
||||
|
||||
r.ParseMultipartForm(10 << 20)
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "Keine Datei gefunden", 400)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
filename := filepath.Base(header.Filename)
|
||||
|
||||
targetPath := filepath.Join("./static/assets", filename)
|
||||
|
||||
// Datei erstellen
|
||||
dst, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
http.Error(w, "Konnte Datei nicht speichern (Rechte?)", 500)
|
||||
return
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// Inhalt kopieren
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
http.Error(w, "Fehler beim Schreiben", 500)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("📂 UPLOAD: %s erfolgreich gespeichert.", filename)
|
||||
|
||||
// JSON Antwort mit dem Pfad zurückgeben
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ok",
|
||||
"filename": filename,
|
||||
})
|
||||
}
|
||||
|
||||
// 2. CONFIG SPEICHERN (Redis)
|
||||
func handleAdminSaveConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Nur POST", 405)
|
||||
return
|
||||
}
|
||||
|
||||
var newConfig GameConfig
|
||||
if err := json.NewDecoder(r.Body).Decode(&newConfig); err != nil {
|
||||
http.Error(w, "Bad JSON", 400)
|
||||
return
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(newConfig)
|
||||
rdb.Set(ctx, "config:gamedata", data, 0)
|
||||
|
||||
defaultConfig.Obstacles = newConfig.Obstacles
|
||||
defaultConfig.Backgrounds = newConfig.Backgrounds
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
64
k8s/app.yaml
64
k8s/app.yaml
@@ -1,14 +1,3 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: escape-game
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
selector:
|
||||
app: escape-game
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
@@ -16,7 +5,7 @@ metadata:
|
||||
labels:
|
||||
app: escape-game
|
||||
spec:
|
||||
replicas: 1
|
||||
replicas: 1 # Kannst du bei RWX auch hochskalieren!
|
||||
selector:
|
||||
matchLabels:
|
||||
app: escape-game
|
||||
@@ -25,9 +14,30 @@ spec:
|
||||
labels:
|
||||
app: escape-game
|
||||
spec:
|
||||
initContainers:
|
||||
- name: init-assets
|
||||
image: ${IMAGE_NAME}
|
||||
command: ["/bin/sh", "-c"]
|
||||
args:
|
||||
- |
|
||||
echo "Prüfe Assets..."
|
||||
# Wenn das Volume leer ist (nur lost+found), kopiere Originale
|
||||
if [ -z "$(ls -A /mnt/assets)" ]; then
|
||||
echo "Volume leer. Kopiere Basis-Assets..."
|
||||
cp -r /app/static/assets/* /mnt/assets/
|
||||
else
|
||||
echo "Assets existieren bereits. Überspringe Copy."
|
||||
# Optional: 'cp -n' nutzen um neue Default-Assets zu ergänzen ohne Uploads zu überschreiben
|
||||
cp -rn /app/static/assets/* /mnt/assets/ || true
|
||||
fi
|
||||
volumeMounts:
|
||||
- name: assets-vol
|
||||
mountPath: /mnt/assets
|
||||
|
||||
# --- MAIN CONTAINER ---
|
||||
containers:
|
||||
- name: game
|
||||
image: ${IMAGE_NAME} # Wird von CI ersetzt
|
||||
image: ${IMAGE_NAME}
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
@@ -37,24 +47,34 @@ spec:
|
||||
value: "${ADMIN_USER}"
|
||||
- name: ADMIN_PASS
|
||||
value: "${ADMIN_PASS}"
|
||||
|
||||
# HIER DAS VOLUME EINHÄNGEN
|
||||
volumeMounts:
|
||||
- name: assets-vol
|
||||
mountPath: /app/static/assets
|
||||
|
||||
resources:
|
||||
requests:
|
||||
memory: "64Mi"
|
||||
cpu: "50m"
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
limits:
|
||||
memory: "256Mi"
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
cpu: "1000m"
|
||||
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 15
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 3
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 20
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
|
||||
volumes:
|
||||
- name: assets-vol
|
||||
persistentVolumeClaim:
|
||||
claimName: game-assets-pvc
|
||||
12
k8s/pvc.yaml
Normal file
12
k8s/pvc.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: game-assets-pvc
|
||||
namespace: ${TARGET_NS}
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
storageClassName: longhorn
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
5
main.go
5
main.go
@@ -48,6 +48,11 @@ func main() {
|
||||
http.HandleFunc("/api/admin/list", Logger(BasicAuth(handleAdminList)))
|
||||
http.HandleFunc("/api/admin/action", Logger(BasicAuth(handleAdminAction)))
|
||||
http.HandleFunc("/api/admin/chunks", Logger(BasicAuth(handleAdminChunks)))
|
||||
http.HandleFunc("/api/admin/upload", Logger(BasicAuth(handleAdminUpload)))
|
||||
http.HandleFunc("/api/admin/save-config", Logger(BasicAuth(handleAdminSaveConfig)))
|
||||
http.HandleFunc("/admin/assets", Logger(BasicAuth(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./secure/assets.html")
|
||||
})))
|
||||
http.HandleFunc("/admin/editor", Logger(BasicAuth(handleEditorPage)))
|
||||
http.HandleFunc("/admin/obstacle_editor", Logger(BasicAuth(handleObstacleEditorPage)))
|
||||
|
||||
|
||||
449
secure/assets.html
Normal file
449
secure/assets.html
Normal 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>
|
||||
@@ -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>
|
||||
27
types.go
27
types.go
@@ -1,18 +1,27 @@
|
||||
package main
|
||||
|
||||
// shared/types.go (oder types.go im Hauptverzeichnis)
|
||||
|
||||
type ObstacleDef struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
Color string `json:"color"`
|
||||
Image string `json:"image"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Width float64 `json:"width"`
|
||||
Height float64 `json:"height"`
|
||||
Color string `json:"color"`
|
||||
Image string `json:"image"`
|
||||
|
||||
// Visuelle Anpassungen
|
||||
ImgScale float64 `json:"imgScale"`
|
||||
ImgOffsetX float64 `json:"imgOffsetX"`
|
||||
ImgOffsetY float64 `json:"imgOffsetY"`
|
||||
|
||||
// Logik
|
||||
CanTalk bool `json:"canTalk"`
|
||||
SpeechLines []string `json:"speechLines"`
|
||||
YOffset float64 `json:"yOffset"`
|
||||
ImgScale float64 `json:"imgScale"`
|
||||
ImgOffsetX float64 `json:"imgOffsetX"`
|
||||
ImgOffsetY float64 `json:"imgOffsetY"`
|
||||
|
||||
// NEU: Verhindert, dass dieses Objekt vom Zufallsgenerator ausgewählt wird
|
||||
NoRandomSpawn bool `json:"noRandomSpawn"`
|
||||
}
|
||||
|
||||
type ChunkObstacle struct {
|
||||
|
||||
@@ -288,6 +288,11 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac
|
||||
var pool []ObstacleDef
|
||||
|
||||
for _, d := range defs {
|
||||
|
||||
if d.NoRandomSpawn {
|
||||
continue
|
||||
}
|
||||
|
||||
if isBoss {
|
||||
if d.ID == "principal" || d.ID == "trashcan" {
|
||||
pool = append(pool, d)
|
||||
|
||||
Reference in New Issue
Block a user