diff --git a/config.go b/config.go index 88eeff4..2384d5f 100644 --- a/config.go +++ b/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() } diff --git a/handlers.go b/handlers.go index ac46bd5..d575d81 100644 --- a/handlers.go +++ b/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) +} diff --git a/k8s/app.yaml b/k8s/app.yaml index 4ce38bb..8f60691 100644 --- a/k8s/app.yaml +++ b/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 - periodSeconds: 10 \ No newline at end of file + initialDelaySeconds: 10 + periodSeconds: 10 + + volumes: + - name: assets-vol + persistentVolumeClaim: + claimName: game-assets-pvc \ No newline at end of file diff --git a/k8s/pvc.yaml b/k8s/pvc.yaml new file mode 100644 index 0000000..88a030d --- /dev/null +++ b/k8s/pvc.yaml @@ -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 \ No newline at end of file diff --git a/main.go b/main.go index 36e2e84..d67d506 100644 --- a/main.go +++ b/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))) diff --git a/secure/assets.html b/secure/assets.html new file mode 100644 index 0000000..bbfc434 --- /dev/null +++ b/secure/assets.html @@ -0,0 +1,449 @@ + + + + + Asset Manager + + + + +
+

📦 ASSET MANAGER

+
Lade Daten...
+ +
+ + + + + + \ No newline at end of file diff --git a/secure/editor.html b/secure/editor.html index d34ee9c..e3ef373 100644 --- a/secure/editor.html +++ b/secure/editor.html @@ -2,319 +2,261 @@ - Ultimate Chunk Editor (+Templates) + Level Chunk Editor