Merge pull request 'update-Credis' (#20) from update-Credis into main
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m9s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 2m9s
Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
1
.github/workflows/deploy.yaml
vendored
1
.github/workflows/deploy.yaml
vendored
@@ -109,6 +109,7 @@ jobs:
|
|||||||
|
|
||||||
# Anwenden
|
# Anwenden
|
||||||
echo "Deploying Resources to Namespace: ${{ env.TARGET_NS }}"
|
echo "Deploying Resources to Namespace: ${{ env.TARGET_NS }}"
|
||||||
|
kubectl apply -f k8s/pvc.yaml -n ${{ env.TARGET_NS }}
|
||||||
kubectl apply -f k8s/redis.yaml -n ${{ env.TARGET_NS }}
|
kubectl apply -f k8s/redis.yaml -n ${{ env.TARGET_NS }}
|
||||||
kubectl apply -f k8s/app.yaml -n ${{ env.TARGET_NS }}
|
kubectl apply -f k8s/app.yaml -n ${{ env.TARGET_NS }}
|
||||||
kubectl apply -f k8s/ingress.yaml -n ${{ env.TARGET_NS }}
|
kubectl apply -f k8s/ingress.yaml -n ${{ env.TARGET_NS }}
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ COPY --from=minifier /minify/game.min.js ./static/js/game.min.js
|
|||||||
# Schritt A: Lösche alle Zeilen, die <script src="js/... enthalten
|
# Schritt A: Lösche alle Zeilen, die <script src="js/... enthalten
|
||||||
# Schritt B: Füge vor dem schließenden </body> Tag das neue Script ein
|
# Schritt B: Füge vor dem schließenden </body> Tag das neue Script ein
|
||||||
RUN sed -i '/<script src="js\//d' static/index.html \
|
RUN sed -i '/<script src="js\//d' static/index.html \
|
||||||
&& sed -i '/<\/body>/i \ <script src="js/game.min.js"></script>' static/index.html
|
&& sed -i '/<\/body>/i \ <script src="js/game.min.js"></script>' static/index.html \
|
||||||
|
&& sed -i '/<\/head>/i \ <script src="assets/pixi.min.js"></script>' static/index.html
|
||||||
|
|
||||||
# Port und Start
|
# Port und Start
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|||||||
53
config.go
53
config.go
@@ -36,32 +36,45 @@ func getEnv(key, fallback string) string {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// config.go
|
||||||
|
|
||||||
func initGameConfig() {
|
func initGameConfig() {
|
||||||
defaultConfig = GameConfig{
|
// 1. Versuche aus Redis zu laden
|
||||||
Obstacles: []ObstacleDef{
|
val, err := rdb.Get(ctx, "config:gamedata").Result()
|
||||||
// --- 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!"}},
|
|
||||||
|
|
||||||
// --- COINS ---
|
if err == nil && val != "" {
|
||||||
{ID: "coin0", Type: "coin", Width: 40, Height: 73, Color: "#ff0000", Image: "coin.png", ImgScale: 1.1, ImgOffsetY: 1},
|
// Redis hat Daten -> Nutzen!
|
||||||
{ID: "coin1", Type: "coin", Width: 40, Height: 73, Color: "#ff0000", Image: "coin.png", YOffset: 60, ImgScale: 1.1, ImgOffsetY: 1},
|
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 ---
|
// --- COINS ---
|
||||||
{ID: "p_god", Type: "powerup", Width: 30, Height: 30, Color: "cyan", Image: "powerup_god1.png", YOffset: 20.0}, // Godmode
|
{ID: "coin0", Type: "coin", Width: 40, Height: 73, Color: "#ff0000", Image: "coin.png", ImgScale: 1.1, ImgOffsetY: 1},
|
||||||
{ID: "p_bat", Type: "powerup", Width: 30, Height: 30, Color: "red", Image: "powerup_bat1.png", YOffset: 20.0}, // Schläger
|
{ID: "coin1", Type: "coin", Width: 40, Height: 73, Color: "#ff0000", Image: "coin.png", YOffset: 60, ImgScale: 1.1, ImgOffsetY: 1},
|
||||||
{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"},
|
// --- 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()
|
defaultConfig.Chunks = loadChunksFromRedis()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
67
handlers.go
67
handlers.go
@@ -3,9 +3,12 @@ package main
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"html"
|
"html"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -344,3 +347,67 @@ func handleAdminBadwords(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
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
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
@@ -16,7 +5,7 @@ metadata:
|
|||||||
labels:
|
labels:
|
||||||
app: escape-game
|
app: escape-game
|
||||||
spec:
|
spec:
|
||||||
replicas: 1
|
replicas: 1 # Kannst du bei RWX auch hochskalieren!
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: escape-game
|
app: escape-game
|
||||||
@@ -25,9 +14,30 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: escape-game
|
app: escape-game
|
||||||
spec:
|
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:
|
containers:
|
||||||
- name: game
|
- name: game
|
||||||
image: ${IMAGE_NAME} # Wird von CI ersetzt
|
image: ${IMAGE_NAME}
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
env:
|
env:
|
||||||
@@ -37,24 +47,34 @@ spec:
|
|||||||
value: "${ADMIN_USER}"
|
value: "${ADMIN_USER}"
|
||||||
- name: ADMIN_PASS
|
- name: ADMIN_PASS
|
||||||
value: "${ADMIN_PASS}"
|
value: "${ADMIN_PASS}"
|
||||||
|
|
||||||
|
# HIER DAS VOLUME EINHÄNGEN
|
||||||
|
volumeMounts:
|
||||||
|
- name: assets-vol
|
||||||
|
mountPath: /app/static/assets
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
requests:
|
requests:
|
||||||
memory: "64Mi"
|
memory: "128Mi"
|
||||||
cpu: "50m"
|
cpu: "100m"
|
||||||
limits:
|
limits:
|
||||||
memory: "256Mi"
|
memory: "512Mi"
|
||||||
cpu: "500m"
|
cpu: "1000m"
|
||||||
|
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
port: 8080
|
port: 8080
|
||||||
initialDelaySeconds: 10
|
initialDelaySeconds: 15
|
||||||
periodSeconds: 15
|
periodSeconds: 20
|
||||||
timeoutSeconds: 5
|
|
||||||
failureThreshold: 3
|
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet:
|
httpGet:
|
||||||
path: /
|
path: /
|
||||||
port: 8080
|
port: 8080
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 10
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- name: assets-vol
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: game-assets-pvc
|
||||||
11
k8s/pvc.yaml
Normal file
11
k8s/pvc.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: game-assets-pvc
|
||||||
|
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/list", Logger(BasicAuth(handleAdminList)))
|
||||||
http.HandleFunc("/api/admin/action", Logger(BasicAuth(handleAdminAction)))
|
http.HandleFunc("/api/admin/action", Logger(BasicAuth(handleAdminAction)))
|
||||||
http.HandleFunc("/api/admin/chunks", Logger(BasicAuth(handleAdminChunks)))
|
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/editor", Logger(BasicAuth(handleEditorPage)))
|
||||||
http.HandleFunc("/admin/obstacle_editor", Logger(BasicAuth(handleObstacleEditorPage)))
|
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">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Ultimate Chunk Editor (+Templates)</title>
|
<title>Level Chunk Editor</title>
|
||||||
<style>
|
<style>
|
||||||
body { background: #1a1a1a; color: #ddd; font-family: 'Segoe UI', monospace; display: flex; height: 100vh; margin: 0; overflow: hidden; }
|
body { background: #1a1a1a; color: #ddd; font-family: 'Segoe UI', monospace; display: flex; height: 100vh; margin: 0; overflow: hidden; }
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
#sidebar {
|
#sidebar {
|
||||||
width: 340px; background: #2a2a2a; padding: 15px; border-right: 2px solid #444;
|
width: 300px; background: #2a2a2a; padding: 15px; border-right: 1px solid #444;
|
||||||
display: flex; flex-direction: column; gap: 10px; overflow-y: auto;
|
display: flex; flex-direction: column; gap: 12px; overflow-y: auto;
|
||||||
box-shadow: 2px 0 10px rgba(0,0,0,0.5);
|
box-shadow: 2px 0 10px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Canvas Bereich */
|
||||||
#canvas-wrapper {
|
#canvas-wrapper {
|
||||||
flex: 1; overflow: auto; position: relative; background: #333;
|
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-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;
|
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.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; }
|
input, select {
|
||||||
.group-title { font-size: 10px; color: #2196F3; font-weight: bold; margin-bottom: 5px; text-transform: uppercase; border-bottom: 1px solid #444; padding-bottom: 2px; }
|
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, select { width: 100%; padding: 4px; background: #1a1a1a; color: white; border: 1px solid #555; margin-bottom: 5px; box-sizing: border-box; border-radius: 3px; font-size: 11px; }
|
}
|
||||||
input:focus { border-color: #ffcc00; outline: none; }
|
input:focus { border-color: #ffcc00; outline: none; }
|
||||||
|
|
||||||
.row { display: flex; gap: 5px; }
|
.row { display: flex; gap: 5px; }
|
||||||
label { font-size: 10px; color: #aaa; display: block; margin-bottom: 1px;}
|
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;}
|
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; }
|
||||||
/* Template Liste Style */
|
.btn-save { background: #4caf50; }
|
||||||
.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; }
|
.btn-del { background: #f44336; margin-top: 10px;}
|
||||||
.template-item:hover { background: #444; }
|
|
||||||
.template-del { color: #f44336; font-weight: bold; padding: 0 5px; cursor: pointer; }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<div id="sidebar">
|
<div id="sidebar">
|
||||||
|
|
||||||
<h3>🎨 VORLAGEN</h3>
|
<h3>📂 LEVEL LADEN</h3>
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div style="display:flex; gap:5px;">
|
<select id="chunk-select"><option value="">Lade Liste...</option></select>
|
||||||
<input type="text" id="tpl-name" placeholder="Name (z.B. Teacher Hut)" style="margin:0;">
|
<div class="row">
|
||||||
<button onclick="saveTemplate()" style="background:#4caf50; border:none; color:white; cursor:pointer; width:40px;">💾</button>
|
<button class="action btn-load" onclick="loadSelectedChunk()">📂 Laden</button>
|
||||||
|
<button class="action" onclick="loadConfig()" style="background:#444; width:40px;">🔄</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<h3>🛠 TOOLS</h3>
|
<h3>🛠 WERKZEUGE (ASSETS)</h3>
|
||||||
<div style="display:grid; grid-template-columns: 1fr 1fr; gap:5px;">
|
<div class="tools-grid" id="tools-container">
|
||||||
<button class="tool-btn active" onclick="setTool('select')">👆 Select</button>
|
<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('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>
|
</div>
|
||||||
|
|
||||||
<h3>⚙️ EIGENSCHAFTEN</h3>
|
<h3>⚙️ EIGENSCHAFTEN</h3>
|
||||||
|
|
||||||
<div id="props" class="panel" style="display:none;">
|
<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" readonly style="color:#777"></div></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 class="row">
|
||||||
<div style="flex:1"><label>X</label><input type="number" id="prop-x" oninput="updateProp()"></div>
|
<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 style="flex:1"><label>Y</label><input type="number" id="prop-y" oninput="updateProp()"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div style="flex:1"><label>Width</label><input type="number" id="prop-w" oninput="updateProp()"></div>
|
<div style="flex:1"><label>Breite</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>Höhe</label><input type="number" id="prop-h" oninput="updateProp()"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="action btn-del" onclick="deleteSelected()">🗑 Entfernen</button>
|
||||||
<div class="group-title" style="margin-top:10px;">🖼️ Optik (Textur)</div>
|
|
||||||
<label>Bild Datei (../../assets/)</label>
|
|
||||||
<input type="text" id="prop-image" oninput="updateProp()">
|
|
||||||
<div class="row">
|
|
||||||
<div style="flex:1"><label>Scale</label><input type="number" step="0.1" id="prop-scale" oninput="updateProp()"></div>
|
|
||||||
<div style="flex:1"><label>Farbe</label><input type="color" id="prop-color" oninput="updateProp()" style="height:25px; padding:0;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div style="flex:1"><label>Off X</label><input type="number" id="prop-imgx" oninput="updateProp()"></div>
|
|
||||||
<div style="flex:1"><label>Off Y</label><input type="number" id="prop-imgy" oninput="updateProp()"></div>
|
|
||||||
</div>
|
|
||||||
<button class="action" onclick="deleteSelected()" style="background:#f44336; margin-top:10px;">🗑 Löschen</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:auto;">
|
<div style="margin-top:auto;">
|
||||||
<h3>💾 LEVEL DATEI</h3>
|
<h3>💾 SPEICHERN</h3>
|
||||||
<div class="panel">
|
<label>Chunk Name (ID)</label>
|
||||||
<select id="chunk-select"><option value="">Lade Liste...</option></select>
|
<input type="text" id="chunk-name" value="new_chunk">
|
||||||
<div class="row">
|
|
||||||
<button class="action" onclick="loadSelectedChunk()" style="background:#2196F3; margin-top:2px;">📂 Laden</button>
|
|
||||||
<button class="action" onclick="refreshList()" style="background:#444; margin-top:2px;">🔄</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<label style="margin-top:5px">Chunk Name</label>
|
<label>Länge (Pixel)</label>
|
||||||
<input type="text" id="chunk-name" value="level_01">
|
<input type="number" id="chunk-width" value="2000">
|
||||||
<label>Länge</label>
|
|
||||||
<input type="number" id="chunk-width" value="2000">
|
|
||||||
|
|
||||||
<button class="action" onclick="saveChunk()" style="background:#4caf50;">💾 Speichern (DB)</button>
|
<button class="action btn-save" onclick="saveChunk()">💾 In DB Speichern</button>
|
||||||
<button class="action" onclick="exportJSON()" style="background:#FF9800;">📋 JSON Copy</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="canvas-wrapper">
|
<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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// --- STATE ---
|
||||||
const canvas = document.getElementById('editorCanvas');
|
const canvas = document.getElementById('editorCanvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d', { alpha: false });
|
||||||
|
|
||||||
// State
|
|
||||||
let elements = [];
|
let elements = [];
|
||||||
let selectedElement = null;
|
let selectedElement = null;
|
||||||
let currentTool = 'select';
|
let currentTool = 'select';
|
||||||
|
|
||||||
|
// Config & Assets
|
||||||
|
let loadedConfig = null;
|
||||||
|
let TOOL_DEFS = {};
|
||||||
|
const imageCache = {};
|
||||||
|
let dpr = 1;
|
||||||
|
|
||||||
|
// Dragging Logic
|
||||||
let isDragging = false;
|
let isDragging = false;
|
||||||
let dragStart = {x:0, y:0};
|
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 ---
|
// --- INIT ---
|
||||||
renderTemplateList(); // Templates laden
|
resizeCanvas();
|
||||||
refreshList();
|
window.addEventListener('resize', resizeCanvas);
|
||||||
|
loadConfig();
|
||||||
requestAnimationFrame(renderLoop);
|
requestAnimationFrame(renderLoop);
|
||||||
|
|
||||||
// --- HELPER ---
|
function resizeCanvas() {
|
||||||
function getImage(path) {
|
const container = canvas.parentElement;
|
||||||
if (!path) return null;
|
dpr = window.devicePixelRatio || 1;
|
||||||
if (imageCache[path]) return imageCache[path];
|
// Interne Größe
|
||||||
const img = new Image();
|
canvas.width = container.clientWidth * dpr;
|
||||||
img.src = "../../assets/" + path;
|
canvas.height = container.clientHeight * dpr;
|
||||||
img.onerror = () => { img.src = "../../assets/" + path; };
|
// CSS Größe
|
||||||
imageCache[path] = img;
|
canvas.style.width = container.clientWidth + 'px';
|
||||||
return img;
|
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() {
|
// Normalize Data
|
||||||
ctx.clearRect(0,0,canvas.width, canvas.height);
|
loadedConfig = {
|
||||||
|
obstacles: data.obstacles || data.Obstacles || [],
|
||||||
|
chunks: data.chunks || data.Chunks || []
|
||||||
|
};
|
||||||
|
|
||||||
// Grid
|
buildToolPalette();
|
||||||
ctx.fillStyle = "rgba(0,0,0,0.5)"; ctx.fillRect(0, 350, canvas.width, 50);
|
buildChunkList();
|
||||||
ctx.strokeStyle = "#aaa"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, 350); ctx.lineTo(canvas.width, 350); ctx.stroke();
|
|
||||||
|
|
||||||
// Start Zone
|
} catch(e) {
|
||||||
ctx.fillStyle = "rgba(0,255,0,0.05)"; ctx.fillRect(0, 0, 800, 350);
|
console.error("Config Fehler:", e);
|
||||||
ctx.fillStyle = "#aaa"; ctx.font = "12px Arial"; ctx.fillText("Viewport Start (800px)", 10, 20);
|
alert("Konnte Config nicht laden.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
elements.forEach(el => {
|
function buildToolPalette() {
|
||||||
const w = el.w; const h = el.h; const x = el.x; const y = el.y;
|
const container = document.getElementById('tools-container');
|
||||||
const scale = el.imgScale || 1.0;
|
// Reset (Select & Platform behalten)
|
||||||
const offX = el.imgOffsetX || 0;
|
container.innerHTML = `
|
||||||
const offY = el.imgOffsetY || 0;
|
<button class="tool-btn active" onclick="setTool('select')">👆 Select</button>
|
||||||
const imgPath = el.image;
|
<button class="tool-btn" onclick="setTool('platform')">🧱 Platform</button>
|
||||||
|
`;
|
||||||
|
|
||||||
// Hitbox
|
// Default Platform Def
|
||||||
ctx.fillStyle = el.color || '#888';
|
TOOL_DEFS['platform'] = { type: 'platform', id: 'plat', w: 150, h: 20, color: '#8B4513' };
|
||||||
if (imgPath) ctx.fillStyle = "rgba(255, 0, 0, 0.2)";
|
|
||||||
ctx.fillRect(x, y, w, h);
|
|
||||||
|
|
||||||
if(el === selectedElement) {
|
// Assets hinzufügen
|
||||||
ctx.strokeStyle = "white"; ctx.lineWidth = 2; ctx.setLineDash([5, 3]);
|
loadedConfig.obstacles.forEach(obs => {
|
||||||
} else {
|
// Speichern in Defs für schnellen Zugriff
|
||||||
ctx.strokeStyle = "rgba(0,0,0,0.3)"; ctx.lineWidth = 1; ctx.setLineDash([]);
|
TOOL_DEFS[obs.id] = obs;
|
||||||
}
|
|
||||||
ctx.strokeRect(x, y, w, h);
|
|
||||||
|
|
||||||
// Bild
|
const btn = document.createElement('button');
|
||||||
const img = getImage(imgPath);
|
btn.className = 'tool-btn';
|
||||||
if (img && img.complete && img.naturalHeight !== 0) {
|
btn.onclick = () => setTool(obs.id);
|
||||||
const drawW = w * scale;
|
|
||||||
const drawH = h * scale;
|
|
||||||
const baseX = x + (w - drawW) / 2;
|
|
||||||
const baseY = y + (h - drawH);
|
|
||||||
const finalX = baseX + offX;
|
|
||||||
const finalY = baseY + offY;
|
|
||||||
|
|
||||||
ctx.drawImage(img, finalX, finalY, drawW, drawH);
|
const imgPath = obs.image ? `../assets/${obs.image}` : '';
|
||||||
|
btn.innerHTML = `
|
||||||
if(el === selectedElement) {
|
<img src="${imgPath}" class="tool-thumb" onerror="this.style.display='none'">
|
||||||
ctx.strokeStyle = "#2196F3"; ctx.lineWidth = 1; ctx.setLineDash([]);
|
${obs.id}
|
||||||
ctx.strokeRect(finalX, finalY, drawW, drawH);
|
`;
|
||||||
}
|
container.appendChild(btn);
|
||||||
}
|
|
||||||
|
|
||||||
// Label
|
|
||||||
ctx.fillStyle = "white"; ctx.font = "bold 10px sans-serif";
|
|
||||||
ctx.shadowColor="black"; ctx.shadowBlur=3;
|
|
||||||
ctx.fillText(el.id, x, y - 4);
|
|
||||||
ctx.shadowBlur=0;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- INTERACTION ---
|
function buildChunkList() {
|
||||||
canvas.addEventListener('mousedown', e => {
|
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 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;
|
const my = e.clientY - rect.top;
|
||||||
|
|
||||||
if (currentTool === 'select') {
|
if (currentTool === 'select') {
|
||||||
|
// Selektieren (Rückwärts, oberstes zuerst)
|
||||||
selectedElement = null;
|
selectedElement = null;
|
||||||
for(let i=elements.length-1; i>=0; i--) {
|
for (let i = elements.length - 1; i >= 0; i--) {
|
||||||
const el = elements[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;
|
selectedElement = el;
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
dragStart = { x: mx - el.x, y: my - el.y };
|
dragStart = { x: mx - el.x, y: my - el.y };
|
||||||
@@ -322,158 +264,192 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const def = DEFAULTS[currentTool];
|
// Neues Objekt platzieren
|
||||||
if(def) {
|
const def = TOOL_DEFS[currentTool];
|
||||||
|
if (def) {
|
||||||
const newEl = {
|
const newEl = {
|
||||||
type: def.type, id: def.id,
|
id: def.id,
|
||||||
x: Math.floor(mx/10)*10, y: Math.floor(my/10)*10,
|
type: def.type,
|
||||||
w: def.w, h: def.h, color: def.color,
|
// Snap to Grid 10px
|
||||||
image: def.image,
|
x: Math.floor(mx / 10) * 10,
|
||||||
imgScale: 1.0, imgOffsetX: 0, imgOffsetY: 0
|
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);
|
elements.push(newEl);
|
||||||
selectedElement = newEl;
|
selectedElement = newEl;
|
||||||
currentTool = 'select';
|
|
||||||
document.querySelectorAll('.tool-btn')[0].click();
|
// Auto-Switch zu Select
|
||||||
|
setTool('select');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateUI();
|
updateUI();
|
||||||
});
|
});
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', e => {
|
canvas.addEventListener('mousemove', e => {
|
||||||
if(isDragging && selectedElement) {
|
if (isDragging && selectedElement) {
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
const rawX = e.clientX - rect.left - dragStart.x;
|
const mx = e.clientX - rect.left;
|
||||||
const rawY = e.clientY - rect.top - dragStart.y;
|
const my = e.clientY - rect.top;
|
||||||
selectedElement.x = Math.floor(rawX / 10) * 10;
|
|
||||||
selectedElement.y = Math.floor(rawY / 10) * 10;
|
selectedElement.x = Math.floor((mx - dragStart.x) / 10) * 10;
|
||||||
|
selectedElement.y = Math.floor((my - dragStart.y) / 10) * 10;
|
||||||
updateUI();
|
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() {
|
function updateUI() {
|
||||||
const props = document.getElementById('props');
|
const p = document.getElementById('props');
|
||||||
if(selectedElement) {
|
if (selectedElement) {
|
||||||
props.style.display = 'block';
|
p.style.display = 'block';
|
||||||
const set = (id, val) => document.getElementById(id).value = val;
|
document.getElementById('prop-id').value = selectedElement.id;
|
||||||
set('prop-id', selectedElement.id || '');
|
document.getElementById('prop-x').value = selectedElement.x;
|
||||||
set('prop-type', selectedElement.type || '');
|
document.getElementById('prop-y').value = selectedElement.y;
|
||||||
set('prop-x', selectedElement.x);
|
document.getElementById('prop-w').value = selectedElement.w;
|
||||||
set('prop-y', selectedElement.y);
|
document.getElementById('prop-h').value = selectedElement.h;
|
||||||
set('prop-w', selectedElement.w);
|
|
||||||
set('prop-h', selectedElement.h);
|
|
||||||
set('prop-image', selectedElement.image || '');
|
|
||||||
set('prop-scale', selectedElement.imgScale || 1.0);
|
|
||||||
set('prop-color', selectedElement.color || '#888888');
|
|
||||||
set('prop-imgx', selectedElement.imgOffsetX || 0);
|
|
||||||
set('prop-imgy', selectedElement.imgOffsetY || 0);
|
|
||||||
} else {
|
} else {
|
||||||
props.style.display = 'none';
|
p.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateProp() {
|
function updateProp() {
|
||||||
if(!selectedElement) return;
|
if (!selectedElement) return;
|
||||||
const get = (id) => document.getElementById(id).value;
|
selectedElement.x = parseFloat(document.getElementById('prop-x').value);
|
||||||
const getNum = (id) => parseFloat(document.getElementById(id).value);
|
selectedElement.y = parseFloat(document.getElementById('prop-y').value);
|
||||||
|
selectedElement.w = parseFloat(document.getElementById('prop-w').value);
|
||||||
selectedElement.id = get('prop-id');
|
selectedElement.h = parseFloat(document.getElementById('prop-h').value);
|
||||||
selectedElement.x = getNum('prop-x');
|
|
||||||
selectedElement.y = getNum('prop-y');
|
|
||||||
selectedElement.w = getNum('prop-w');
|
|
||||||
selectedElement.h = getNum('prop-h');
|
|
||||||
selectedElement.image = get('prop-image');
|
|
||||||
selectedElement.imgScale = getNum('prop-scale');
|
|
||||||
selectedElement.color = get('prop-color');
|
|
||||||
selectedElement.imgOffsetX = getNum('prop-imgx');
|
|
||||||
selectedElement.imgOffsetY = getNum('prop-imgy');
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTool(t) {
|
|
||||||
currentTool = t;
|
|
||||||
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
|
|
||||||
const btns = document.querySelectorAll('.tool-btn');
|
|
||||||
for(let btn of btns) {
|
|
||||||
if(btn.innerText.toLowerCase().includes(t)) btn.classList.add('active');
|
|
||||||
}
|
|
||||||
selectedElement = null;
|
|
||||||
updateUI();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteSelected() {
|
function deleteSelected() {
|
||||||
if(!selectedElement) return;
|
if (!selectedElement) return;
|
||||||
elements = elements.filter(e => e !== selectedElement);
|
elements = elements.filter(e => e !== selectedElement);
|
||||||
selectedElement = null;
|
selectedElement = null;
|
||||||
updateUI();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SAVE / LOAD (DB) ---
|
// --- 6. SAVE / LOAD ---
|
||||||
async function refreshList() {
|
|
||||||
const sel = document.getElementById('chunk-select');
|
|
||||||
sel.innerHTML = "<option>Lade...</option>";
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/config');
|
|
||||||
loadedConfig = await res.json();
|
|
||||||
sel.innerHTML = "";
|
|
||||||
if(loadedConfig.chunks && loadedConfig.chunks.length > 0) {
|
|
||||||
loadedConfig.chunks.forEach(c => {
|
|
||||||
const opt = document.createElement('option');
|
|
||||||
opt.value = c.id;
|
|
||||||
opt.innerText = c.id + " (L=" + (c.totalWidth||'?') + ")";
|
|
||||||
sel.appendChild(opt);
|
|
||||||
});
|
|
||||||
} else { sel.innerHTML = "<option value=''>Keine Chunks</option>"; }
|
|
||||||
} catch(e) { console.error(e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadSelectedChunk() {
|
function loadSelectedChunk() {
|
||||||
const id = document.getElementById('chunk-select').value;
|
const id = document.getElementById('chunk-select').value;
|
||||||
if(!id || !loadedConfig) return;
|
if (!id) return;
|
||||||
|
|
||||||
const chunk = loadedConfig.chunks.find(c => c.id === id);
|
const chunk = loadedConfig.chunks.find(c => c.id === id);
|
||||||
if(!chunk) return;
|
if (!chunk) return alert("Chunk nicht gefunden");
|
||||||
if(!confirm("Editor überschreiben?")) return;
|
|
||||||
|
if (!confirm("Editor leeren und Chunk laden?")) return;
|
||||||
|
|
||||||
document.getElementById('chunk-name').value = chunk.id;
|
document.getElementById('chunk-name').value = chunk.id;
|
||||||
document.getElementById('chunk-width').value = chunk.totalWidth || 2000;
|
document.getElementById('chunk-width').value = chunk.totalWidth || 2000;
|
||||||
|
|
||||||
elements = [];
|
elements = [];
|
||||||
const merge = (item, typeDefault) => ({
|
const adder = (item, type) => {
|
||||||
type: item.type || typeDefault,
|
elements.push({
|
||||||
id: item.id || 'obj',
|
id: item.id || 'plat', // Plattformen haben oft keine ID
|
||||||
x: item.x, y: item.y, w: item.w, h: item.h,
|
type: type,
|
||||||
color: item.color || '#888',
|
x: item.x, y: item.y, w: item.w, h: item.h,
|
||||||
image: item.image || '',
|
_def: TOOL_DEFS[item.id] // Verknüpfung wiederherstellen
|
||||||
imgScale: item.imgScale || 1.0,
|
});
|
||||||
imgOffsetX: item.imgOffsetX || 0,
|
};
|
||||||
imgOffsetY: item.imgOffsetY || 0
|
|
||||||
});
|
|
||||||
|
|
||||||
if(chunk.platforms) chunk.platforms.forEach(p => elements.push(merge(p, 'platform')));
|
if (chunk.platforms) chunk.platforms.forEach(p => adder(p, 'platform'));
|
||||||
if(chunk.obstacles) chunk.obstacles.forEach(o => elements.push(merge(o, 'obstacle')));
|
if (chunk.obstacles) chunk.obstacles.forEach(o => adder(o, o.type)); // Type aus Objekt oder Fallback
|
||||||
|
|
||||||
selectedElement = null;
|
selectedElement = null;
|
||||||
updateUI();
|
updateUI();
|
||||||
alert(`Chunk '${id}' geladen!`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveChunk() {
|
async function saveChunk() {
|
||||||
const name = document.getElementById('chunk-name').value;
|
const name = document.getElementById('chunk-name').value;
|
||||||
const width = parseInt(document.getElementById('chunk-width').value) || 2000;
|
const width = parseInt(document.getElementById('chunk-width').value) || 2000;
|
||||||
|
|
||||||
for(let el of elements) {
|
// Sauberes JSON bauen (ohne _def Referenzen)
|
||||||
if(!el.id) { alert("Fehler: ID fehlt!"); return; }
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapObj = (el) => ({
|
const mapObj = (el) => ({
|
||||||
id: el.id, type: el.type, x: el.x, y: el.y, w: el.w, h: el.h, color: el.color,
|
id: el.id,
|
||||||
image: el.image, imgScale: el.imgScale, imgOffsetX: el.imgOffsetX, imgOffsetY: el.imgOffsetY
|
type: el.type,
|
||||||
|
x: el.x, y: el.y, w: el.w, h: el.h
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
@@ -485,17 +461,14 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/admin/chunks', {
|
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");
|
else alert("Fehler beim Speichern");
|
||||||
} catch(e) { alert("Netzwerkfehler"); }
|
} 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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
2312
static/assets/pixi.min.js
vendored
Normal file
2312
static/assets/pixi.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2312
static/assets/pixi.min.mjs
Normal file
2312
static/assets/pixi.min.mjs
Normal file
File diff suppressed because one or more lines are too long
@@ -5,31 +5,33 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
|
||||||
<title>Escape the Teacher</title>
|
<title>Escape the Teacher</title>
|
||||||
|
|
||||||
|
<script src="assets/pixi.min.js"></script>
|
||||||
|
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<button id="mute-btn" onclick="toggleAudioClick()">🔊</button>
|
<button id="mute-btn" onclick="toggleAudioClick()">🔊</button>
|
||||||
|
|
||||||
<div id="rotate-overlay">
|
<div id="rotate-overlay">
|
||||||
<div class="icon">📱↻</div>
|
<div class="icon">📱↻</div>
|
||||||
<p>Bitte Gerät drehen!</p>
|
<p>Bitte Gerät drehen!</p>
|
||||||
<small>Querformat benötigt</small>
|
<small>Querformat benötigt</small>
|
||||||
</div>
|
</div>
|
||||||
<div id="game-container">
|
|
||||||
<canvas id="gameCanvas"></canvas>
|
|
||||||
|
|
||||||
|
<div id="game-container">
|
||||||
<div id="ui-layer">
|
<div id="ui-layer">
|
||||||
SCORE: <span id="score">0</span>
|
SCORE: <span id="score">0</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="startScreen">
|
<div id="startScreen">
|
||||||
|
|
||||||
<div class="start-left">
|
<div class="start-left">
|
||||||
<h1>ESCAPE THE<br>TEACHER</h1>
|
<h1>ESCAPE THE<br>TEACHER</h1>
|
||||||
|
|
||||||
<p style="font-size: 12px; color: #aaa;">Dein Rekord: <span id="localHighscore" style="color:yellow">0</span></p>
|
<p style="font-size: 12px; color: #aaa;">Dein Rekord: <span id="localHighscore" style="color:yellow">0</span></p>
|
||||||
|
|
||||||
<button id="startBtn" onclick="startGameClick()">STARTEN</button>
|
<button id="startBtn" onclick="startGameClick()">STARTEN</button>
|
||||||
<div id="loadingText">Lade Grafiken...</div>
|
<div id="loadingText">Lade Assets...</div>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<div class="info-title">SCHUL-NEWS</div>
|
<div class="info-title">SCHUL-NEWS</div>
|
||||||
@@ -78,13 +80,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="modal-codes" class="modal-overlay">
|
<div id="modal-codes" class="modal-overlay" style="display:none;">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<button class="close-modal" onclick="closeModal()">X</button>
|
<button class="close-modal" onclick="closeModal()">X</button>
|
||||||
<h2 style="color:yellow">MEINE BEWEISE</h2>
|
<h2 style="color:yellow">MEINE BEWEISE</h2>
|
||||||
<div id="codesList" style="font-size: 10px; line-height: 1.8;">
|
<div id="codesList" style="font-size: 10px; line-height: 1.8;">
|
||||||
|
Lade Daten...
|
||||||
</div>
|
</div>
|
||||||
<p style="margin-top:20px; font-size:9px; color:#888;">Zeige diesen Code für deinen Preis oder lösche den Eintrag.</p>
|
<p style="margin-top:20px; font-size:9px; color:#888;">Zeige diesen Code dem Lehrer für deinen Preis oder lösche den Eintrag.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -97,12 +100,12 @@
|
|||||||
Sebastian Unterschütz<br>
|
Sebastian Unterschütz<br>
|
||||||
Göltzschtalblick 16<br>
|
Göltzschtalblick 16<br>
|
||||||
08236 Ellefeld<br>
|
08236 Ellefeld<br>
|
||||||
<small>Kontakt: sebastian@unterschutz.de</small>
|
<small>Kontakt: sebastian@unterschuetz.de</small>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr style="border:1px solid #444; margin: 15px 0;">
|
<hr style="border:1px solid #444; margin: 15px 0;">
|
||||||
|
|
||||||
<p><strong>🎵 Musik Design:</strong><br>
|
<p><strong>🎵 Musik & Sound Design:</strong><br>
|
||||||
<span style="color:#ffcc00; font-size:18px;">Max E.</span>
|
<span style="color:#ffcc00; font-size:18px;">Max E.</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -129,50 +132,43 @@
|
|||||||
<h2>Datenschutzerklärung</h2>
|
<h2>Datenschutzerklärung</h2>
|
||||||
|
|
||||||
<p><strong>1. Datenschutz auf einen Blick</strong><br>
|
<p><strong>1. Datenschutz auf einen Blick</strong><br>
|
||||||
Allgemeine Hinweise: Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen. Personenbezogene Daten sind alle Daten, mit denen Sie persönlich identifiziert werden können.</p>
|
Allgemeine Hinweise: Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen.</p>
|
||||||
|
|
||||||
<p><strong>2. Verantwortlicher</strong><br>
|
<p><strong>2. Verantwortlicher</strong><br>
|
||||||
Verantwortlich für die Datenverarbeitung auf dieser Website ist:<br>
|
Verantwortlich für die Datenverarbeitung auf dieser Website ist:<br>
|
||||||
Sebastian Unterschütz<br>
|
Sebastian Unterschütz<br>
|
||||||
Göltzschtalblick 16, 08236 Ellefeld<br>
|
Göltzschtalblick 16, 08236 Ellefeld<br>
|
||||||
E-Mail: sebastian@unterschutz.de<br>
|
E-Mail: sebastian@unterschuetz.de<br>
|
||||||
<em>(Schulprojekt im Rahmen der IT232)</em></p>
|
<em>(Schulprojekt im Rahmen der IT232)</em></p>
|
||||||
|
|
||||||
<p><strong>3. Hosting (Hetzner)</strong><br>
|
<p><strong>3. Hosting (Hetzner)</strong><br>
|
||||||
Wir hosten die Inhalte unserer Website bei folgendem Anbieter:<br>
|
Wir hosten die Inhalte unserer Website bei folgendem Anbieter:<br>
|
||||||
<strong>Hetzner Online GmbH</strong><br>
|
<strong>Hetzner Online GmbH</strong><br>
|
||||||
Industriestr. 25<br>
|
Industriestr. 25, 91710 Gunzenhausen, Deutschland<br>
|
||||||
91710 Gunzenhausen<br>
|
|
||||||
Deutschland<br>
|
|
||||||
<br>
|
<br>
|
||||||
Serverstandort: <strong>Deutschland</strong> (ausschließlich).<br>
|
Serverstandort: <strong>Deutschland</strong> (ausschließlich).<br>
|
||||||
Wir haben mit dem Anbieter einen Vertrag zur Auftragsverarbeitung (AVV) geschlossen. Hierbei handelt es sich um einen datenschutzrechtlich vorgeschriebenen Vertrag, der gewährleistet, dass dieser die personenbezogenen Daten unserer Websitebesucher nur nach unseren Weisungen und unter Einhaltung der DSGVO verarbeitet.</p>
|
Wir haben mit dem Anbieter einen Vertrag zur Auftragsverarbeitung (AVV) geschlossen, der die Einhaltung der DSGVO gewährleistet.</p>
|
||||||
|
|
||||||
<p><strong>4. Datenerfassung auf dieser Website</strong></p>
|
<p><strong>4. Datenerfassung auf dieser Website</strong></p>
|
||||||
|
|
||||||
<p><strong>Server-Log-Dateien</strong><br>
|
<p><strong>Server-Log-Dateien</strong><br>
|
||||||
Der Provider der Seiten (Hetzner) erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien, die Ihr Browser automatisch an uns übermittelt. Dies sind:
|
Der Provider der Seiten (Hetzner) erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien (Browser, OS, Referrer, Hostname, Uhrzeit, IP-Adresse).<br>
|
||||||
<ul>
|
<strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse an technischer Fehlerfreiheit und Sicherheit). Die Daten werden nach spätestens 14 Tagen gelöscht.</p>
|
||||||
<li>Uhrzeit der Serveranfrage</li>
|
|
||||||
<li>IP-Adresse</li>
|
|
||||||
</ul>
|
|
||||||
Eine Zusammenführung dieser Daten mit anderen Datenquellen wird nicht vorgenommen.<br>
|
|
||||||
<strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. f DSGVO. Der Websitebetreiber hat ein berechtigtes Interesse an der technisch fehlerfreien Darstellung und der Optimierung seiner Website – hierzu müssen die Server-Log-Files erfasst werden. Die Daten werden nach spätestens 7 Tagen automatisch gelöscht.</p>
|
|
||||||
|
|
||||||
<p><strong>Spielstände & Highscores</strong><br>
|
<p><strong>Spielstände & Highscores</strong><br>
|
||||||
Wenn Sie einen Highscore eintragen, speichern wir in unserer Datenbank:
|
Wenn Sie einen Highscore eintragen, speichern wir:
|
||||||
<ul>
|
<ul>
|
||||||
<li>Den von Ihnen gewählten Namen (Pseudonym empfohlen!)</li>
|
<li>Gewählter Name (Pseudonym empfohlen!)</li>
|
||||||
<li>Ihren Punktestand und Zeitstempel</li>
|
<li>Punktestand und Zeitstempel</li>
|
||||||
<li>Eine Session-ID und einen "Claim-Code" zur Verifizierung</li>
|
<li>Session-ID und "Claim-Code"</li>
|
||||||
</ul>
|
</ul>
|
||||||
Diese Daten dienen ausschließlich der Darstellung der Bestenliste und der Spielmechanik.</p>
|
Diese Daten dienen der Darstellung der Bestenliste.</p>
|
||||||
|
|
||||||
<p><strong>Lokale Speicherung (LocalStorage)</strong><br>
|
<p><strong>Lokale Speicherung (LocalStorage)</strong><br>
|
||||||
Das Spiel speichert Einstellungen (z.B. "Ton aus") und Ihre persönlichen "Claim-Codes" lokal in Ihrem Browser (`LocalStorage`). Diese Daten verlassen Ihr Gerät nicht, außer Sie übermitteln einen Highscore aktiv an den Server. Wir setzen <strong>keine Tracking-Cookies</strong> oder Analyse-Tools (wie Google Analytics) ein.</p>
|
Das Spiel speichert Einstellungen (Audio) und Codes lokal in Ihrem Browser. Wir setzen <strong>keine Tracking-Cookies</strong> oder Analyse-Tools ein.</p>
|
||||||
|
|
||||||
<p><strong>5. Ihre Rechte</strong><br>
|
<p><strong>5. Ihre Rechte</strong><br>
|
||||||
Sie haben jederzeit das Recht auf unentgeltliche Auskunft über Ihre gespeicherten personenbezogenen Daten, deren Herkunft und Empfänger und den Zweck der Datenverarbeitung sowie ein Recht auf Berichtigung oder Löschung dieser Daten. Hierzu sowie zu weiteren Fragen zum Thema personenbezogene Daten können Sie sich jederzeit an die im Impressum angegebene Adresse wenden.</p>
|
Sie haben jederzeit das Recht auf Auskunft, Berichtigung und Löschung Ihrer Daten. Wenden Sie sich dazu an den Verantwortlichen im Impressum.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -202,5 +198,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -14,12 +14,20 @@ SOUNDS.hit.volume = 0.6;
|
|||||||
SOUNDS.music.loop = true;
|
SOUNDS.music.loop = true;
|
||||||
SOUNDS.music.volume = 0.2;
|
SOUNDS.music.volume = 0.2;
|
||||||
|
|
||||||
|
// Standard: Pitch beibehalten (WICHTIG für euch!)
|
||||||
|
if (SOUNDS.music.preservesPitch !== undefined) {
|
||||||
|
SOUNDS.music.preservesPitch = true;
|
||||||
|
} else if (SOUNDS.music.mozPreservesPitch !== undefined) {
|
||||||
|
SOUNDS.music.mozPreservesPitch = true; // Firefox Fallback
|
||||||
|
} else if (SOUNDS.music.webkitPreservesPitch !== undefined) {
|
||||||
|
SOUNDS.music.webkitPreservesPitch = true; // Safari Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mute Status laden
|
||||||
let isMuted = localStorage.getItem('escape_muted') === 'true';
|
let isMuted = localStorage.getItem('escape_muted') === 'true';
|
||||||
|
|
||||||
function playSound(name) {
|
function playSound(name) {
|
||||||
if (isMuted || !SOUNDS[name]) return;
|
if (isMuted || !SOUNDS[name]) return;
|
||||||
|
|
||||||
const soundClone = SOUNDS[name].cloneNode();
|
const soundClone = SOUNDS[name].cloneNode();
|
||||||
soundClone.volume = SOUNDS[name].volume;
|
soundClone.volume = SOUNDS[name].volume;
|
||||||
soundClone.play().catch(() => {});
|
soundClone.play().catch(() => {});
|
||||||
@@ -27,28 +35,41 @@ function playSound(name) {
|
|||||||
|
|
||||||
function toggleMute() {
|
function toggleMute() {
|
||||||
isMuted = !isMuted;
|
isMuted = !isMuted;
|
||||||
|
|
||||||
|
|
||||||
localStorage.setItem('escape_muted', isMuted);
|
localStorage.setItem('escape_muted', isMuted);
|
||||||
|
|
||||||
|
if(isMuted) SOUNDS.music.pause();
|
||||||
if(isMuted) {
|
else SOUNDS.music.play().catch(()=>{});
|
||||||
SOUNDS.music.pause();
|
|
||||||
} else {
|
|
||||||
SOUNDS.music.play().catch(()=>{});
|
|
||||||
}
|
|
||||||
|
|
||||||
return isMuted;
|
return isMuted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startMusic() {
|
function startMusic() {
|
||||||
|
if(!isMuted) SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked"));
|
||||||
if(!isMuted) {
|
|
||||||
SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked", e));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getMuteState() {
|
function getMuteState() {
|
||||||
return isMuted;
|
return isMuted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- GESCHWINDIGKEIT ANPASSEN ---
|
||||||
|
function setMusicSpeed(gameSpeed) {
|
||||||
|
if (isMuted || !SOUNDS.music) return;
|
||||||
|
|
||||||
|
const baseGameSpeed = 15.0; // Muss zu BASE_SPEED in config.js passen
|
||||||
|
|
||||||
|
// Faktor berechnen: Speed 30 = Musik 1.3x
|
||||||
|
let rate = 1.0 + (gameSpeed - baseGameSpeed) * 0.02;
|
||||||
|
|
||||||
|
// Limits
|
||||||
|
if (rate < 1.0) rate = 1.0;
|
||||||
|
if (rate > 2.0) rate = 2.0;
|
||||||
|
|
||||||
|
// Nur bei spürbarer Änderung anwenden
|
||||||
|
if (Math.abs(SOUNDS.music.playbackRate - rate) > 0.05) {
|
||||||
|
SOUNDS.music.playbackRate = rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetMusicSpeed() {
|
||||||
|
if (SOUNDS.music) SOUNDS.music.playbackRate = 1.0;
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// INPUT HANDLING (WEBSOCKET VERSION)
|
// INPUT HANDLING
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
function handleInput(action, active) {
|
function handleInput(action, active) {
|
||||||
@@ -8,24 +8,22 @@ function handleInput(action, active) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JUMP
|
||||||
if (action === "JUMP" && active) {
|
if (action === "JUMP" && active) {
|
||||||
if (player.grounded && !isCrouching) {
|
if (player.grounded && !isCrouching) {
|
||||||
|
|
||||||
player.vy = JUMP_POWER;
|
player.vy = JUMP_POWER;
|
||||||
player.grounded = false;
|
player.grounded = false;
|
||||||
|
|
||||||
playSound('jump');
|
if (typeof playSound === 'function') playSound('jump');
|
||||||
spawnParticles(player.x + 15, player.y + 50, 'dust', 5); // Staubwolke an den Füßen
|
if (typeof spawnParticles === 'function') spawnParticles(player.x + 15, player.y + 50, 'dust', 5);
|
||||||
|
|
||||||
if (typeof sendInput === "function") {
|
if (typeof sendInput === "function") sendInput("input", "JUMP");
|
||||||
sendInput("input", "JUMP");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DUCK
|
||||||
if (action === "DUCK") {
|
if (action === "DUCK") {
|
||||||
const wasCrouching = isCrouching;
|
const wasCrouching = isCrouching;
|
||||||
|
|
||||||
isCrouching = active;
|
isCrouching = active;
|
||||||
|
|
||||||
if (wasCrouching !== isCrouching) {
|
if (wasCrouching !== isCrouching) {
|
||||||
@@ -37,67 +35,75 @@ function handleInput(action, active) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// EVENT LISTENERS
|
// EVENT LISTENERS (KEYBOARD)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
|
|
||||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||||
|
|
||||||
if (e.code === 'Space' || e.code === 'ArrowUp') handleInput("JUMP", true);
|
if (e.code === 'Space' || e.code === 'ArrowUp') handleInput("JUMP", true);
|
||||||
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", true);
|
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", true);
|
||||||
if (e.code === 'F9') {
|
|
||||||
e.preventDefault();
|
|
||||||
console.log("🐞 Fordere Debug-Daten vom Server an...");
|
|
||||||
if (typeof sendInput === "function") {
|
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
||||||
socket.send(JSON.stringify({ type: "debug" }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('keyup', (e) => {
|
window.addEventListener('keyup', (e) => {
|
||||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||||
|
|
||||||
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", false);
|
if (e.code === 'ArrowDown' || e.code === 'KeyS') handleInput("DUCK", false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Maus / Touch (Einfach)
|
|
||||||
window.addEventListener('mousedown', (e) => {
|
window.addEventListener('mousedown', (e) => {
|
||||||
// Nur Linksklick und nur auf dem Canvas
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT') return;
|
||||||
if (e.target === canvas && e.button === 0) handleInput("JUMP", true);
|
if (e.button === 0) handleInput("JUMP", true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Touch (Swipe Gesten)
|
|
||||||
let touchStartY = 0;
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// TOUCH HANDLING (INSTANT RESPONSE)
|
||||||
|
// ==========================================
|
||||||
|
let touchStartY = 0;
|
||||||
|
let isSwiping = false;
|
||||||
|
|
||||||
|
// 1. TOUCH START -> SOFORT SPRINGEN
|
||||||
window.addEventListener('touchstart', (e) => {
|
window.addEventListener('touchstart', (e) => {
|
||||||
if(e.target === canvas) {
|
// UI Buttons ignorieren
|
||||||
e.preventDefault();
|
if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT') return;
|
||||||
touchStartY = e.touches[0].clientY;
|
e.preventDefault();
|
||||||
|
|
||||||
|
touchStartY = e.touches[0].clientY;
|
||||||
|
isSwiping = false;
|
||||||
|
|
||||||
|
// FIX: Wir springen SOFORT, ohne zu warten!
|
||||||
|
// Das gibt das "Snappy" Gefühl.
|
||||||
|
handleInput("JUMP", true);
|
||||||
|
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
// 2. TOUCH MOVE -> DUCKEN ERKENNEN
|
||||||
|
window.addEventListener('touchmove', (e) => {
|
||||||
|
if (e.target.tagName === 'BUTTON') return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (isSwiping) return; // Nur einmal pro Swipe auslösen
|
||||||
|
|
||||||
|
const currentY = e.touches[0].clientY;
|
||||||
|
const diff = currentY - touchStartY;
|
||||||
|
|
||||||
|
// Wenn wir mehr als 30px nach UNTEN wischen...
|
||||||
|
if (diff > 30) {
|
||||||
|
// ... korrigieren wir den Sprung in ein Ducken!
|
||||||
|
// Da wir im Spiel eine Physik haben, wo Ducken in der Luft
|
||||||
|
// zu "Fast Fall" (schnellem Fallen) führt, fühlt sich das gut an.
|
||||||
|
|
||||||
|
handleInput("DUCK", true);
|
||||||
|
isSwiping = true; // Sperren bis zum nächsten Touch
|
||||||
|
|
||||||
|
// Automatisch aufstehen nach kurzer Zeit
|
||||||
|
setTimeout(() => handleInput("DUCK", false), 800);
|
||||||
}
|
}
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
|
|
||||||
|
// 3. TOUCH END -> RESET
|
||||||
|
// (Hier müssen wir nichts mehr tun, da der Jump schon beim Start passiert ist)
|
||||||
window.addEventListener('touchend', (e) => {
|
window.addEventListener('touchend', (e) => {
|
||||||
if(e.target === canvas) {
|
if (e.target.tagName === 'BUTTON') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const touchEndY = e.changedTouches[0].clientY;
|
// Ggf. Ducken beenden falls man gedrückt hält?
|
||||||
const diff = touchEndY - touchStartY;
|
// Hier lassen wir den Timeout regeln.
|
||||||
|
}, { passive: false });
|
||||||
// Nach oben wischen oder Tippen = Sprung
|
|
||||||
if (diff < -30) {
|
|
||||||
handleInput("JUMP", true);
|
|
||||||
}
|
|
||||||
// Nach unten wischen = Ducken (kurz)
|
|
||||||
else if (diff > 30) {
|
|
||||||
handleInput("DUCK", true);
|
|
||||||
setTimeout(() => handleInput("DUCK", false), 800);
|
|
||||||
}
|
|
||||||
// Einfaches Tippen (wenig Bewegung) = Sprung
|
|
||||||
else if (Math.abs(diff) < 10) {
|
|
||||||
handleInput("JUMP", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -3,6 +3,9 @@ function updateGameLogic() {
|
|||||||
if (currentSpeed > 36.0) currentSpeed = 36.0;
|
if (currentSpeed > 36.0) currentSpeed = 36.0;
|
||||||
|
|
||||||
updateParticles();
|
updateParticles();
|
||||||
|
if (typeof setMusicSpeed === "function") {
|
||||||
|
setMusicSpeed(currentSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
player.prevY = player.y;
|
player.prevY = player.y;
|
||||||
|
|||||||
@@ -1,63 +1,95 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// 1. ASSETS LADEN
|
// 1. ASSETS LADEN (PIXI V8)
|
||||||
|
// ==========================================
|
||||||
|
// ==========================================
|
||||||
|
// 1. ASSETS LADEN (PIXI V8 KORREKT)
|
||||||
|
// ==========================================
|
||||||
|
// ==========================================
|
||||||
|
// 1. ASSETS LADEN (FEHLERTOLERANT)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
async function loadAssets() {
|
async function loadAssets() {
|
||||||
const pPromise = new Promise(resolve => {
|
const assetsToLoad = [];
|
||||||
playerSprite.src = "assets/player.png";
|
|
||||||
playerSprite.onload = resolve;
|
|
||||||
playerSprite.onerror = () => { resolve(); };
|
|
||||||
});
|
|
||||||
|
|
||||||
const bgPromises = gameConfig.backgrounds.map((bgFile, index) => {
|
// --- 1. REGISTRIEREN (Namen zu Pfaden zuordnen) ---
|
||||||
return new Promise((resolve) => {
|
|
||||||
const img = new Image();
|
// A. Player
|
||||||
img.src = "assets/" + bgFile;
|
// Prüfen ob schon da, um Warnungen bei Hot-Reload zu vermeiden
|
||||||
img.onload = () => { bgSprites[index] = img; resolve(); };
|
if (!PIXI.Assets.cache.has('player')) {
|
||||||
img.onerror = () => { resolve(); };
|
PIXI.Assets.add({ alias: 'player', src: 'assets/player.png' });
|
||||||
|
assetsToLoad.push('player');
|
||||||
|
}
|
||||||
|
|
||||||
|
// B. Hintergründe
|
||||||
|
if (gameConfig.backgrounds) {
|
||||||
|
gameConfig.backgrounds.forEach(bg => {
|
||||||
|
if (!PIXI.Assets.cache.has(bg)) {
|
||||||
|
PIXI.Assets.add({ alias: bg, src: 'assets/' + bg });
|
||||||
|
assetsToLoad.push(bg);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
const obsPromises = gameConfig.obstacles.map(def => {
|
// C. Hindernisse
|
||||||
return new Promise((resolve) => {
|
if (gameConfig.obstacles) {
|
||||||
if (!def.image) { resolve(); return; }
|
gameConfig.obstacles.forEach(def => {
|
||||||
const img = new Image();
|
if (def.image && !PIXI.Assets.cache.has(def.id)) {
|
||||||
img.src = "assets/" + def.image;
|
PIXI.Assets.add({ alias: def.id, src: 'assets/' + def.image });
|
||||||
img.onload = () => { sprites[def.id] = img; resolve(); };
|
assetsToLoad.push(def.id);
|
||||||
img.onerror = () => { resolve(); };
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 2. LADEN (Mit Fehler-Abfangung) ---
|
||||||
|
console.log(`Lade ${assetsToLoad.length} Assets...`);
|
||||||
|
|
||||||
|
// Wir erstellen für jedes Asset einen eigenen Lade-Versuch
|
||||||
|
const loadPromises = assetsToLoad.map(key => {
|
||||||
|
return PIXI.Assets.load(key)
|
||||||
|
.catch(err => {
|
||||||
|
// HIER IST DER TRICK:
|
||||||
|
// Wenn ein Fehler passiert, loggen wir ihn, aber werfen ihn NICHT weiter.
|
||||||
|
// Wir geben einfach null zurück. Damit gilt dieser Task als "erledigt".
|
||||||
|
console.warn(`⚠️ Asset Fehler bei '${key}': Datei fehlt oder defekt.`);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all([pPromise, ...bgPromises, ...obsPromises]);
|
// Wir warten, bis ALLE Versuche durch sind (egal ob Erfolg oder Fehler)
|
||||||
|
await Promise.all(loadPromises);
|
||||||
|
|
||||||
|
console.log("✅ Ladevorgang abgeschlossen (vorhandene Assets sind bereit).");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ... (Rest der Datei: startGameClick, gameLoop etc. BLEIBT GLEICH)
|
||||||
|
// ...
|
||||||
window.startGameClick = async function() {
|
window.startGameClick = async function() {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
|
|
||||||
startScreen.style.display = 'none';
|
startScreen.style.display = 'none';
|
||||||
document.body.classList.add('game-active');
|
document.body.classList.add('game-active');
|
||||||
|
|
||||||
// Score Reset visuell
|
|
||||||
score = 0;
|
score = 0;
|
||||||
const scoreEl = document.getElementById('score');
|
document.getElementById('score').innerText = "0";
|
||||||
if (scoreEl) scoreEl.innerText = "0";
|
|
||||||
|
if (typeof startMusic === 'function') startMusic();
|
||||||
|
|
||||||
// WebSocket Start
|
|
||||||
startMusic();
|
|
||||||
connectGame();
|
connectGame();
|
||||||
resize();
|
resize();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. GAME OVER & SCORE
|
||||||
|
// ==========================================
|
||||||
window.gameOver = function(reason) {
|
window.gameOver = function(reason) {
|
||||||
if (isGameOver) return;
|
if (isGameOver) return;
|
||||||
isGameOver = true;
|
isGameOver = true;
|
||||||
console.log("Game Over:", reason);
|
console.log("Game Over:", reason);
|
||||||
|
|
||||||
|
// Highscore Check (Lokal)
|
||||||
const finalScore = Math.floor(score / 10);
|
const finalScore = Math.floor(score / 10);
|
||||||
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
|
const currentHighscore = localStorage.getItem('escape_highscore') || 0;
|
||||||
|
|
||||||
if (finalScore > currentHighscore) {
|
if (finalScore > parseInt(currentHighscore)) {
|
||||||
localStorage.setItem('escape_highscore', finalScore);
|
localStorage.setItem('escape_highscore', finalScore);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,22 +97,24 @@ window.gameOver = function(reason) {
|
|||||||
gameOverScreen.style.display = 'flex';
|
gameOverScreen.style.display = 'flex';
|
||||||
document.getElementById('finalScore').innerText = finalScore;
|
document.getElementById('finalScore').innerText = finalScore;
|
||||||
|
|
||||||
|
// Input Reset
|
||||||
document.getElementById('inputSection').style.display = 'flex';
|
document.getElementById('inputSection').style.display = 'flex';
|
||||||
document.getElementById('submitBtn').disabled = false;
|
document.getElementById('submitBtn').disabled = false;
|
||||||
|
document.getElementById('playerNameInput').value = "";
|
||||||
|
|
||||||
|
// Liste laden
|
||||||
loadLeaderboard();
|
loadLeaderboard();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
window.submitScore = async function() {
|
window.submitScore = async function() {
|
||||||
const nameInput = document.getElementById('playerNameInput');
|
const nameInput = document.getElementById('playerNameInput');
|
||||||
const name = nameInput.value.trim();
|
const name = nameInput.value.trim();
|
||||||
const btn = document.getElementById('submitBtn');
|
const btn = document.getElementById('submitBtn');
|
||||||
|
|
||||||
if (!name) return alert("Bitte Namen eingeben!");
|
if (!name) return alert("Bitte Namen eingeben!");
|
||||||
|
if (!sessionID) return alert("Fehler: Keine Session ID vom Server erhalten.");
|
||||||
|
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -90,11 +124,10 @@ window.submitScore = async function() {
|
|||||||
body: JSON.stringify({ sessionId: sessionID, name: name })
|
body: JSON.stringify({ sessionId: sessionID, name: name })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) throw new Error("Fehler beim Senden");
|
if (!res.ok) throw new Error("Server antwortet nicht");
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Lokal speichern ("Meine Codes")
|
||||||
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||||
myClaims.push({
|
myClaims.push({
|
||||||
name: name,
|
name: name,
|
||||||
@@ -105,52 +138,104 @@ window.submitScore = async function() {
|
|||||||
});
|
});
|
||||||
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
|
localStorage.setItem('escape_claims', JSON.stringify(myClaims));
|
||||||
|
|
||||||
|
// UI Update
|
||||||
document.getElementById('inputSection').style.display = 'none';
|
document.getElementById('inputSection').style.display = 'none';
|
||||||
loadLeaderboard();
|
loadLeaderboard();
|
||||||
alert(`Gespeichert! Dein Code: ${data.claimCode}`);
|
alert(`Gespeichert! Dein Code: ${data.claimCode}`);
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
alert("Fehler: " + e.message);
|
||||||
alert("Fehler beim Speichern: " + e.message);
|
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
async function loadLeaderboard() {
|
async function loadLeaderboard() {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
|
const res = await fetch(`/api/leaderboard?sessionId=${sessionID}`);
|
||||||
const entries = await res.json();
|
const entries = await res.json();
|
||||||
|
|
||||||
let html = "<h3 style='margin-bottom:5px; color:#ffcc00;'>BESTENLISTE</h3>";
|
let html = "<h3 style='margin-bottom:5px; color:#ffcc00;'>BESTENLISTE</h3>";
|
||||||
|
|
||||||
if(entries.length === 0) html += "<div>Noch keine Einträge.</div>";
|
if(!entries || entries.length === 0) html += "<div>Leer.</div>";
|
||||||
|
else entries.forEach(e => {
|
||||||
entries.forEach(e => {
|
const color = e.isMe ? "cyan" : "white";
|
||||||
const color = e.isMe ? "cyan" : "white"; // Eigener Name in Cyan
|
const bg = e.isMe ? "background:rgba(0,255,255,0.1);" : "";
|
||||||
const bgStyle = e.isMe ? "background:rgba(0,255,255,0.1);" : "";
|
html += `<div style="display:flex; justify-content:space-between; color:${color}; ${bg} padding:4px; border-bottom:1px dotted #444; font-size:12px;">
|
||||||
|
<span>#${e.rank} ${e.name}</span><span>${Math.floor(e.score/10)}</span></div>`;
|
||||||
html += `
|
|
||||||
<div style="border-bottom:1px dotted #444; padding:5px; ${bgStyle} display:flex; justify-content:space-between; color:${color}; font-size:12px;">
|
|
||||||
<span>#${e.rank} ${e.name}</span>
|
|
||||||
<span>${Math.floor(e.score/10)}</span>
|
|
||||||
</div>`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('leaderboard').innerHTML = html;
|
document.getElementById('leaderboard').innerHTML = html;
|
||||||
} catch(e) {
|
} catch(e) { console.error(e); }
|
||||||
console.error("Leaderboard Error:", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. MEINE CODES (LOGIK)
|
||||||
|
// ==========================================
|
||||||
|
window.showMyCodes = function() {
|
||||||
|
openModal('codes');
|
||||||
|
const listEl = document.getElementById('codesList');
|
||||||
|
if(!listEl) return;
|
||||||
|
|
||||||
|
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||||
|
if (rawClaims.length === 0) {
|
||||||
|
listEl.innerHTML = "<div style='padding:20px; text-align:center; color:#666;'>Keine Codes.</div>";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedClaims = rawClaims.sort((a, b) => b.score - a.score);
|
||||||
|
let html = "";
|
||||||
|
|
||||||
|
sortedClaims.forEach(c => {
|
||||||
|
let rankIcon = "📄";
|
||||||
|
if (c.score >= 5000) rankIcon = "⭐";
|
||||||
|
if (c.score >= 10000) rankIcon = "🔥";
|
||||||
|
|
||||||
|
html += `
|
||||||
|
<div style="border-bottom:1px solid #444; padding:8px 0; display:flex; justify-content:space-between; align-items:center;">
|
||||||
|
<div style="text-align:left;">
|
||||||
|
<span style="color:#00e5ff; font-weight:bold; font-size:12px;">${rankIcon} ${c.code}</span>
|
||||||
|
<span style="color:#ffcc00; font-weight:bold;">(${c.score} Pkt)</span><br>
|
||||||
|
<span style="color:#aaa; font-size:9px;">${c.name} • ${c.date}</span>
|
||||||
|
</div>
|
||||||
|
<button onclick="deleteClaim('${c.code}')"
|
||||||
|
style="background:transparent; border:1px solid #ff4444; color:#ff4444; padding:4px 8px; font-size:9px; cursor:pointer;">
|
||||||
|
LÖSCHEN
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
listEl.innerHTML = html;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.deleteClaim = async function(code) {
|
||||||
|
if(!confirm("Eintrag wirklich löschen?")) return;
|
||||||
|
|
||||||
|
// Suchen der SessionID für den Server-Call
|
||||||
|
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||||
|
const item = claims.find(c => c.code === code);
|
||||||
|
|
||||||
|
if (item && item.sessionId) {
|
||||||
|
try {
|
||||||
|
await fetch('/api/claim/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({ sessionId: item.sessionId, claimCode: code })
|
||||||
|
});
|
||||||
|
} catch(e) { console.warn("Server delete failed"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lokal löschen
|
||||||
|
claims = claims.filter(c => c.code !== code);
|
||||||
|
localStorage.setItem('escape_claims', JSON.stringify(claims));
|
||||||
|
window.showMyCodes(); // Refresh
|
||||||
|
};
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 5. GAME LOOP (PHYSICS + RENDER)
|
||||||
|
// ==========================================
|
||||||
function gameLoop(timestamp) {
|
function gameLoop(timestamp) {
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
|
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
|
|
||||||
|
// Nur updaten, wenn Spiel läuft
|
||||||
if (isGameRunning && !isGameOver) {
|
if (isGameRunning && !isGameOver) {
|
||||||
if (!lastTime) lastTime = timestamp;
|
if (!lastTime) lastTime = timestamp;
|
||||||
const deltaTime = timestamp - lastTime;
|
const deltaTime = timestamp - lastTime;
|
||||||
@@ -160,46 +245,62 @@ function gameLoop(timestamp) {
|
|||||||
|
|
||||||
accumulator += deltaTime;
|
accumulator += deltaTime;
|
||||||
|
|
||||||
|
// --- FIXED TIME STEP (Physik: 20 TPS) ---
|
||||||
while (accumulator >= MS_PER_TICK) {
|
while (accumulator >= MS_PER_TICK) {
|
||||||
updateGameLogic();
|
updateGameLogic(); // logic.js (setzt prevX/prevY)
|
||||||
currentTick++;
|
currentTick++;
|
||||||
|
|
||||||
|
// Score lokal hochzählen (damit es flüssig aussieht)
|
||||||
|
// Server korrigiert, falls Abweichung zu groß
|
||||||
score++;
|
score++;
|
||||||
|
|
||||||
accumulator -= MS_PER_TICK;
|
accumulator -= MS_PER_TICK;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alpha = accumulator / MS_PER_TICK;
|
// HUD Update
|
||||||
|
|
||||||
// Score im HUD
|
|
||||||
const scoreEl = document.getElementById('score');
|
const scoreEl = document.getElementById('score');
|
||||||
if (scoreEl) scoreEl.innerText = Math.floor(score / 10);
|
if (scoreEl) scoreEl.innerText = Math.floor(score / 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
drawGame(isGameRunning ? accumulator / MS_PER_TICK : 1.0);
|
// --- INTERPOLATION (Rendering: 60+ FPS) ---
|
||||||
|
// alpha berechnen: Wie viel % ist seit dem letzten Tick vergangen?
|
||||||
|
const alpha = (isGameRunning && !isGameOver) ? (accumulator / MS_PER_TICK) : 1.0;
|
||||||
|
|
||||||
|
// drawGame ist jetzt in render.js und nutzt Pixi
|
||||||
|
drawGame(alpha);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 6. INITIALISIERUNG
|
||||||
|
// ==========================================
|
||||||
async function initGame() {
|
async function initGame() {
|
||||||
try {
|
try {
|
||||||
const cRes = await fetch('/api/config');
|
const cRes = await fetch('/api/config');
|
||||||
gameConfig = await cRes.json();
|
gameConfig = await cRes.json();
|
||||||
|
|
||||||
|
// Pixi Assets laden
|
||||||
await loadAssets();
|
await loadAssets();
|
||||||
await loadStartScreenLeaderboard();
|
|
||||||
|
|
||||||
if (typeof getMuteState === 'function') {
|
// Startscreen Bestenliste
|
||||||
updateMuteIcon(getMuteState());
|
await loadStartScreenLeaderboard();
|
||||||
}
|
|
||||||
|
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
if(loadingText) loadingText.style.display = 'none';
|
if(loadingText) loadingText.style.display = 'none';
|
||||||
if(startBtn) startBtn.style.display = 'inline-block';
|
if(startBtn) startBtn.style.display = 'inline-block';
|
||||||
|
|
||||||
|
// Mute Icon setzen (audio.js State)
|
||||||
|
if (typeof getMuteState === 'function') {
|
||||||
|
updateMuteIcon(getMuteState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lokaler Highscore
|
||||||
const savedHighscore = localStorage.getItem('escape_highscore') || 0;
|
const savedHighscore = localStorage.getItem('escape_highscore') || 0;
|
||||||
const hsEl = document.getElementById('localHighscore');
|
const hsEl = document.getElementById('localHighscore');
|
||||||
if(hsEl) hsEl.innerText = savedHighscore;
|
if(hsEl) hsEl.innerText = savedHighscore;
|
||||||
|
|
||||||
|
// Loop starten (für Idle Rendering)
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
drawGame();
|
drawGame(1.0); // Initiale Zeichnung
|
||||||
|
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -207,16 +308,14 @@ async function initGame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: Startscreen Leaderboard
|
||||||
async function loadStartScreenLeaderboard() {
|
async function loadStartScreenLeaderboard() {
|
||||||
try {
|
try {
|
||||||
const listEl = document.getElementById('startLeaderboardList');
|
const listEl = document.getElementById('startLeaderboardList');
|
||||||
if (!listEl) return;
|
if (!listEl) return;
|
||||||
const res = await fetch('/api/leaderboard');
|
const res = await fetch('/api/leaderboard');
|
||||||
const entries = await res.json();
|
const entries = await res.json();
|
||||||
|
if (!entries || entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Noch keine Scores.</div>"; return; }
|
||||||
if (entries.length === 0) { listEl.innerHTML = "<div style='padding:20px'>Keine Scores.</div>"; return; }
|
|
||||||
|
|
||||||
let html = "";
|
let html = "";
|
||||||
entries.forEach(e => {
|
entries.forEach(e => {
|
||||||
let icon = "#" + e.rank;
|
let icon = "#" + e.rank;
|
||||||
@@ -227,14 +326,13 @@ async function loadStartScreenLeaderboard() {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper: Audio Button Logik
|
||||||
window.toggleAudioClick = function() {
|
window.toggleAudioClick = function() {
|
||||||
const muted = toggleMute();
|
if (typeof toggleMute === 'function') {
|
||||||
|
const muted = toggleMute();
|
||||||
updateMuteIcon(muted);
|
updateMuteIcon(muted);
|
||||||
|
document.getElementById('mute-btn').blur();
|
||||||
|
}
|
||||||
document.getElementById('mute-btn').blur();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateMuteIcon(isMuted) {
|
function updateMuteIcon(isMuted) {
|
||||||
@@ -246,100 +344,10 @@ function updateMuteIcon(isMuted) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modal Helpers
|
||||||
|
window.openModal = function(id) { document.getElementById('modal-' + id).style.display = 'flex'; }
|
||||||
|
window.closeModal = function() { document.querySelectorAll('.modal-overlay').forEach(el => el.style.display = 'none'); }
|
||||||
|
window.onclick = function(event) { if (event.target.classList.contains('modal-overlay')) closeModal(); }
|
||||||
|
|
||||||
window.showMyCodes = function() {
|
// Start
|
||||||
|
|
||||||
openModal('codes');
|
|
||||||
|
|
||||||
const listEl = document.getElementById('codesList');
|
|
||||||
if(!listEl) return;
|
|
||||||
|
|
||||||
|
|
||||||
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
|
||||||
|
|
||||||
if (rawClaims.length === 0) {
|
|
||||||
listEl.innerHTML = "<div style='padding:20px; text-align:center; color:#666;'>Keine Codes gespeichert.</div>";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const sortedClaims = rawClaims
|
|
||||||
.map((item, index) => ({ ...item, originalIndex: index }))
|
|
||||||
.sort((a, b) => b.score - a.score);
|
|
||||||
|
|
||||||
let html = "";
|
|
||||||
|
|
||||||
sortedClaims.forEach(c => {
|
|
||||||
|
|
||||||
let rankIcon = "📄";
|
|
||||||
if (c.score >= 5000) rankIcon = "⭐";
|
|
||||||
if (c.score >= 10000) rankIcon = "🔥";
|
|
||||||
if (c.score >= 20000) rankIcon = "👑";
|
|
||||||
|
|
||||||
html += `
|
|
||||||
<div style="border-bottom:1px solid #444; padding:10px 0; display:flex; justify-content:space-between; align-items:center;">
|
|
||||||
<div style="text-align:left;">
|
|
||||||
<span style="color:#00e5ff; font-weight:bold; font-size:14px;">${rankIcon} ${c.code}</span>
|
|
||||||
<span style="color:#ffcc00; font-weight:bold;">(${c.score} Pkt)</span><br>
|
|
||||||
<span style="color:#aaa; font-size:10px;">${c.name} • ${c.date}</span>
|
|
||||||
</div>
|
|
||||||
<button onclick="deleteClaim('${c.sessionId}', '${c.code}')"
|
|
||||||
style="background:transparent; border:1px solid #ff4444; color:#ff4444; padding:5px 10px; font-size:10px; cursor:pointer;">
|
|
||||||
LÖSCHEN
|
|
||||||
</button>
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
|
|
||||||
listEl.innerHTML = html;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
window.deleteClaim = async function(sid, code) {
|
|
||||||
if(!confirm("Eintrag wirklich löschen?")) return;
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetch('/api/claim/delete', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify({ sessionId: sid, claimCode: code })
|
|
||||||
});
|
|
||||||
} catch(e) {
|
|
||||||
console.warn("Server Delete fehlgeschlagen (vielleicht schon weg), lösche lokal...");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
|
||||||
|
|
||||||
claims = claims.filter(c => c.code !== code);
|
|
||||||
|
|
||||||
localStorage.setItem('escape_claims', JSON.stringify(claims));
|
|
||||||
|
|
||||||
|
|
||||||
window.showMyCodes();
|
|
||||||
|
|
||||||
|
|
||||||
if(document.getElementById('startLeaderboardList')) {
|
|
||||||
loadStartScreenLeaderboard();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
window.openModal = function(id) {
|
|
||||||
const el = document.getElementById('modal-' + id);
|
|
||||||
if(el) el.style.display = 'flex';
|
|
||||||
}
|
|
||||||
|
|
||||||
window.closeModal = function() {
|
|
||||||
const modals = document.querySelectorAll('.modal-overlay');
|
|
||||||
modals.forEach(el => el.style.display = 'none');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
window.onclick = function(event) {
|
|
||||||
if (event.target.classList.contains('modal-overlay')) {
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initGame();
|
initGame();
|
||||||
@@ -1,20 +1,55 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// RESIZE LOGIK (LETTERBOXING)
|
// PIXI INITIALISIERUNG (V8)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
function resize() {
|
let floorGraphic = null;
|
||||||
canvas.width = GAME_WIDTH; // 800
|
async function initPixi() {
|
||||||
canvas.height = GAME_HEIGHT; // 400
|
if (app) return;
|
||||||
|
|
||||||
|
app = new PIXI.Application();
|
||||||
|
|
||||||
|
// 1. Asynchrones Init (v8 Standard)
|
||||||
|
await app.init({
|
||||||
|
width: GAME_WIDTH,
|
||||||
|
height: GAME_HEIGHT,
|
||||||
|
backgroundColor: 0x1a1a1a, // Dunkelgrau
|
||||||
|
preference: 'webgpu', // Versuch WebGPU (schneller auf Handy!)
|
||||||
|
resolution: Math.min(window.devicePixelRatio || 1, 2), // Retina Limit
|
||||||
|
autoDensity: true,
|
||||||
|
antialias: false,
|
||||||
|
roundPixels: true // Wichtig für Pixelart
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Canvas einhängen
|
||||||
|
document.getElementById('game-container').appendChild(app.canvas);
|
||||||
|
|
||||||
|
// 3. Layer erstellen
|
||||||
|
bgLayer = new PIXI.Container();
|
||||||
|
gameLayer = new PIXI.Container();
|
||||||
|
debugLayer = new PIXI.Graphics(); // Für Hitboxen
|
||||||
|
|
||||||
|
// Sortierung aktivieren (damit Player vor Obstacles ist)
|
||||||
|
gameLayer.sortableChildren = true;
|
||||||
|
|
||||||
|
app.stage.addChild(bgLayer);
|
||||||
|
app.stage.addChild(gameLayer);
|
||||||
|
app.stage.addChild(debugLayer);
|
||||||
|
|
||||||
|
// Einmalig Resizen
|
||||||
|
resize();
|
||||||
|
|
||||||
|
console.log(`🚀 Renderer: ${app.renderer.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize() {
|
||||||
|
if (!app || !app.canvas) return;
|
||||||
|
|
||||||
// 2. Verfügbaren Platz berechnen
|
|
||||||
const windowWidth = window.innerWidth - 20;
|
const windowWidth = window.innerWidth - 20;
|
||||||
const windowHeight = window.innerHeight - 20;
|
const windowHeight = window.innerHeight - 20;
|
||||||
|
|
||||||
const targetRatio = GAME_WIDTH / GAME_HEIGHT;
|
const targetRatio = GAME_WIDTH / GAME_HEIGHT;
|
||||||
const windowRatio = windowWidth / windowHeight;
|
const windowRatio = windowWidth / windowHeight;
|
||||||
|
|
||||||
let finalWidth, finalHeight;
|
let finalWidth, finalHeight;
|
||||||
|
|
||||||
// 3. Skalierung berechnen (Aspect Ratio erhalten)
|
|
||||||
if (windowRatio < targetRatio) {
|
if (windowRatio < targetRatio) {
|
||||||
finalWidth = windowWidth;
|
finalWidth = windowWidth;
|
||||||
finalHeight = windowWidth / targetRatio;
|
finalHeight = windowWidth / targetRatio;
|
||||||
@@ -23,163 +58,265 @@ function resize() {
|
|||||||
finalWidth = finalHeight * targetRatio;
|
finalWidth = finalHeight * targetRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Container Größe setzen (Canvas füllt Container via CSS)
|
// CSS Skalierung
|
||||||
if (container) {
|
app.canvas.style.width = `${Math.floor(finalWidth)}px`;
|
||||||
container.style.width = `${Math.floor(finalWidth)}px`;
|
app.canvas.style.height = `${Math.floor(finalHeight)}px`;
|
||||||
container.style.height = `${Math.floor(finalHeight)}px`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('resize', resize);
|
window.addEventListener('resize', resize);
|
||||||
resize();
|
|
||||||
|
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// RENDER LOOP (RETAINED MODE)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
function drawGame(alpha = 1.0) {
|
async function drawGame(alpha = 1.0) {
|
||||||
|
if (!app) {
|
||||||
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
if(!document.getElementById('game-container').querySelector('canvas')) await initPixi();
|
||||||
|
return;
|
||||||
let currentBg = null;
|
|
||||||
if (bgSprites.length > 0) {
|
|
||||||
const changeInterval = 10000;
|
|
||||||
const currentRawIndex = Math.floor(score / changeInterval);
|
|
||||||
if (currentRawIndex > maxRawBgIndex) maxRawBgIndex = currentRawIndex;
|
|
||||||
const bgIndex = maxRawBgIndex % bgSprites.length;
|
|
||||||
currentBg = bgSprites[bgIndex];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentBg && currentBg.complete && currentBg.naturalHeight !== 0) {
|
// 1. HINTERGRUND
|
||||||
ctx.drawImage(currentBg, 0, 0, GAME_WIDTH, GAME_HEIGHT);
|
updateBackground();
|
||||||
|
|
||||||
|
// 2. BODEN (FIX: Einmalig erstellen oder updaten)
|
||||||
|
if (!floorGraphic) {
|
||||||
|
floorGraphic = new PIXI.Graphics();
|
||||||
|
bgLayer.addChild(floorGraphic); // Zum Background Layer hinzufügen
|
||||||
|
}
|
||||||
|
floorGraphic.clear();
|
||||||
|
// Boden: Dunkelgrau
|
||||||
|
floorGraphic.rect(0, GROUND_Y, GAME_WIDTH, 50).fill(0x333333);
|
||||||
|
// Grüne Linie oben drauf (Gras/Teppich)
|
||||||
|
floorGraphic.rect(0, GROUND_Y, GAME_WIDTH, 4).fill(0x4CAF50);
|
||||||
|
|
||||||
|
|
||||||
|
// 3. OBJEKTE SYNCEN
|
||||||
|
syncSprites(obstacles, spriteCache, 'obstacle', alpha);
|
||||||
|
syncSprites(platforms, platformCache, 'platform', alpha);
|
||||||
|
|
||||||
|
// 4. SPIELER
|
||||||
|
updatePlayer(alpha);
|
||||||
|
|
||||||
|
// 5. DEBUG
|
||||||
|
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
||||||
|
drawDebugOverlay(alpha);
|
||||||
} else {
|
} else {
|
||||||
ctx.fillStyle = "#f0f0f0";
|
debugLayer.clear();
|
||||||
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// HELPER: SYNC SYSTEM
|
||||||
|
// ------------------------------------------------------
|
||||||
|
function syncSprites(dataList, cacheMap, type, alpha) {
|
||||||
|
const usedObjects = new Set();
|
||||||
|
|
||||||
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
|
dataList.forEach(obj => {
|
||||||
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
|
usedObjects.add(obj);
|
||||||
|
let sprite = cacheMap.get(obj);
|
||||||
|
|
||||||
|
if (!sprite) {
|
||||||
|
sprite = createPixiSprite(obj, type);
|
||||||
|
gameLayer.addChild(sprite);
|
||||||
|
cacheMap.set(obj, sprite);
|
||||||
|
}
|
||||||
|
|
||||||
platforms.forEach(p => {
|
const def = obj.def || {};
|
||||||
|
|
||||||
const rX = (p.prevX !== undefined) ? lerp(p.prevX, p.x, alpha) : p.x;
|
const rX = (obj.prevX !== undefined) ? lerp(obj.prevX, obj.x, alpha) : obj.x;
|
||||||
const rY = p.y;
|
const rY = obj.y;
|
||||||
|
|
||||||
|
if (obj.speech) {
|
||||||
|
let bubble = sprite.children.find(c => c.label === "bubble");
|
||||||
|
|
||||||
ctx.fillStyle = "#5D4037";
|
if (!bubble) {
|
||||||
ctx.fillRect(rX, rY, p.w, p.h);
|
bubble = createSpeechBubble(obj.speech);
|
||||||
ctx.fillStyle = "#8D6E63";
|
bubble.y = -10;
|
||||||
ctx.fillRect(rX, rY, p.w, 5);
|
if (def.height) bubble.y = -5;
|
||||||
});
|
|
||||||
|
|
||||||
|
sprite.addChild(bubble);
|
||||||
obstacles.forEach(obs => {
|
}
|
||||||
const def = obs.def || {};
|
|
||||||
const img = sprites[def.id];
|
|
||||||
|
|
||||||
const rX = (obs.prevX !== undefined) ? lerp(obs.prevX, obs.x, alpha) : obs.x;
|
|
||||||
const rY = obs.y;
|
|
||||||
|
|
||||||
const hbw = def.width || obs.w || 30;
|
|
||||||
const hbh = def.height || obs.h || 30;
|
|
||||||
|
|
||||||
if (img && img.complete && img.naturalHeight !== 0) {
|
|
||||||
|
|
||||||
const scale = def.imgScale || 1.0;
|
|
||||||
const offX = def.imgOffsetX || 0.0;
|
|
||||||
const offY = def.imgOffsetY || 0.0;
|
|
||||||
|
|
||||||
const drawW = hbw * scale;
|
|
||||||
const drawH = hbh * scale;
|
|
||||||
|
|
||||||
const baseX = rX + (hbw - drawW) / 2;
|
|
||||||
const baseY = rY + (hbh - drawH);
|
|
||||||
|
|
||||||
ctx.drawImage(img, baseX + offX, baseY + offY, drawW, drawH);
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let color = "#FF00FF";
|
// Keine Sprache mehr? Blase entfernen falls vorhanden
|
||||||
if (def.type === "coin") color = "gold";
|
const bubble = sprite.children.find(c => c.label === "bubble");
|
||||||
else if (def.color) color = def.color;
|
if (bubble) {
|
||||||
|
sprite.removeChild(bubble);
|
||||||
ctx.fillStyle = color;
|
bubble.destroy();
|
||||||
ctx.fillRect(rX, rY, hbw, hbh);
|
}
|
||||||
|
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.5)"; ctx.lineWidth = 2;
|
|
||||||
ctx.strokeRect(rX, rY, hbw, hbh);
|
|
||||||
ctx.fillStyle = "white"; ctx.font = "bold 10px monospace";
|
|
||||||
ctx.fillText(def.id || "?", rX, rY - 5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
if (type === 'platform') {
|
||||||
ctx.strokeStyle = "rgba(0,255,0,0.5)"; // Grün transparent
|
sprite.x = rX;
|
||||||
ctx.lineWidth = 1;
|
sprite.y = rY;
|
||||||
ctx.strokeRect(rX, rY, hbw, hbh);
|
} else {
|
||||||
}
|
// Editor Werte
|
||||||
|
const scale = def.imgScale || 1.0;
|
||||||
|
const offX = def.imgOffsetX || 0;
|
||||||
|
const offY = def.imgOffsetY || 0;
|
||||||
|
const hbw = def.width || 30;
|
||||||
|
const hbh = def.height || 30;
|
||||||
|
|
||||||
if(obs.speech) drawSpeechBubble(rX, rY, obs.speech);
|
const drawW = hbw * scale;
|
||||||
|
const baseX = rX + (hbw - drawW) / 2;
|
||||||
|
const baseY = rY + (hbh - (hbh * scale));
|
||||||
|
|
||||||
|
sprite.x = baseX + offX;
|
||||||
|
sprite.y = baseY + offY;
|
||||||
|
sprite.width = drawW;
|
||||||
|
sprite.height = hbh * scale;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// C. Aufräumen (Garbage Collection)
|
||||||
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
for (const [obj, sprite] of cacheMap.entries()) {
|
||||||
if (serverObstacles) {
|
if (!usedObjects.has(obj)) {
|
||||||
ctx.strokeStyle = "cyan";
|
gameLayer.removeChild(sprite);
|
||||||
ctx.lineWidth = 1;
|
sprite.destroy();
|
||||||
serverObstacles.forEach(sObj => {
|
cacheMap.delete(obj);
|
||||||
ctx.strokeRect(sObj.x, sObj.y, sObj.w, sObj.h);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let rPlayerY = lerp(player.prevY !== undefined ? player.prevY : player.y, player.y, alpha);
|
|
||||||
|
|
||||||
|
|
||||||
const drawY = isCrouching ? rPlayerY + 25 : rPlayerY;
|
|
||||||
const drawH = isCrouching ? 25 : 50;
|
|
||||||
|
|
||||||
if (playerSprite.complete && playerSprite.naturalHeight !== 0) {
|
|
||||||
ctx.drawImage(playerSprite, player.x, drawY, player.w, drawH);
|
|
||||||
} else {
|
|
||||||
ctx.fillStyle = player.color;
|
|
||||||
ctx.fillRect(player.x, drawY, player.w, drawH);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof drawParticles === 'function') {
|
|
||||||
drawParticles();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (isGameRunning && !isGameOver) {
|
|
||||||
ctx.fillStyle = "black";
|
|
||||||
ctx.font = "bold 10px monospace";
|
|
||||||
ctx.textAlign = "left";
|
|
||||||
let statusText = "";
|
|
||||||
|
|
||||||
if(godModeLives > 0) statusText += `🛡️ x${godModeLives} `;
|
|
||||||
if(hasBat) statusText += `⚾ BAT `;
|
|
||||||
if(bootTicks > 0) statusText += `👟 ${(bootTicks/60).toFixed(1)}s`;
|
|
||||||
|
|
||||||
if(statusText !== "") {
|
|
||||||
ctx.fillText(statusText, 10, 40);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isGameOver) {
|
|
||||||
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
|
||||||
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Sprechblase zeichnen
|
function createPixiSprite(obj, type) {
|
||||||
function drawSpeechBubble(x, y, text) {
|
if (type === 'platform') {
|
||||||
const bX = x - 20;
|
const g = new PIXI.Graphics();
|
||||||
const bY = y - 40;
|
// Holz Plattform
|
||||||
const bW = 120;
|
g.rect(0, 0, obj.w, obj.h).fill(0x8B4513); // Braun
|
||||||
const bH = 30;
|
g.rect(0, 0, obj.w, 5).fill(0xA0522D); // Hellbraun Oben
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const def = obj.def || {};
|
||||||
|
|
||||||
ctx.fillStyle = "white"; ctx.fillRect(bX, bY, bW, bH);
|
// CHECK: Ist die Textur im Cache?
|
||||||
ctx.strokeStyle = "black"; ctx.lineWidth = 1; ctx.strokeRect(bX, bY, bW, bH);
|
// Wir nutzen PIXI.Assets.get(), das ist sicherer als cache.has
|
||||||
ctx.fillStyle = "black"; ctx.font = "10px Arial"; ctx.textAlign = "center";
|
let texture = null;
|
||||||
ctx.fillText(text, bX + bW/2, bY + 20);
|
try {
|
||||||
|
if (def.id) texture = PIXI.Assets.get(def.id);
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
if (texture) {
|
||||||
|
const s = new PIXI.Sprite(texture);
|
||||||
|
return s;
|
||||||
|
} else {
|
||||||
|
// FALLBACK (Wenn Bild fehlt -> Magenta Box)
|
||||||
|
const g = new PIXI.Graphics();
|
||||||
|
let color = 0xFF00FF;
|
||||||
|
if (def.type === 'coin') color = 0xFFD700;
|
||||||
|
|
||||||
|
g.rect(0, 0, def.width||30, def.height||30).fill(color);
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// PLAYER & BG
|
||||||
|
// ------------------------------------------------------
|
||||||
|
function updatePlayer(alpha) {
|
||||||
|
if (!pixiPlayer) {
|
||||||
|
if (PIXI.Assets.cache.has('player')) {
|
||||||
|
pixiPlayer = PIXI.Sprite.from('player');
|
||||||
|
} else {
|
||||||
|
pixiPlayer = new PIXI.Graphics().rect(0,0,30,50).fill(0xFF0000);
|
||||||
|
}
|
||||||
|
gameLayer.addChild(pixiPlayer);
|
||||||
|
pixiPlayer.zIndex = 100; // Immer im Vordergrund
|
||||||
|
}
|
||||||
|
|
||||||
|
let rY = lerp(player.prevY || player.y, player.y, alpha);
|
||||||
|
const drawY = isCrouching ? rY + 25 : rY;
|
||||||
|
|
||||||
|
pixiPlayer.x = player.x;
|
||||||
|
pixiPlayer.y = drawY;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateBackground() {
|
||||||
|
// FEHLERBEHEBUNG:
|
||||||
|
// Wir prüfen 'gameConfig.backgrounds' statt 'bgSprites'
|
||||||
|
if (!gameConfig || !gameConfig.backgrounds || gameConfig.backgrounds.length === 0) return;
|
||||||
|
|
||||||
|
const changeInterval = 10000;
|
||||||
|
const idx = Math.floor(score / changeInterval) % gameConfig.backgrounds.length;
|
||||||
|
|
||||||
|
// Der Key ist der Dateiname (so haben wir es in main.js geladen)
|
||||||
|
const bgKey = gameConfig.backgrounds[idx];
|
||||||
|
|
||||||
|
// Sicherstellen, dass Asset geladen ist
|
||||||
|
if (!PIXI.Assets.cache.has(bgKey)) return;
|
||||||
|
|
||||||
|
if (!bgSprite) {
|
||||||
|
bgSprite = new PIXI.Sprite();
|
||||||
|
bgSprite.width = GAME_WIDTH;
|
||||||
|
bgSprite.height = GAME_HEIGHT;
|
||||||
|
bgLayer.addChild(bgSprite);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Textur wechseln wenn nötig
|
||||||
|
const tex = PIXI.Assets.get(bgKey);
|
||||||
|
if (tex && bgSprite.texture !== tex) {
|
||||||
|
bgSprite.texture = tex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------
|
||||||
|
// DEBUG
|
||||||
|
// ------------------------------------------------------
|
||||||
|
function drawDebugOverlay(alpha) {
|
||||||
|
const g = debugLayer;
|
||||||
|
g.clear();
|
||||||
|
|
||||||
|
// Server (Cyan)
|
||||||
|
if (serverObstacles) {
|
||||||
|
serverObstacles.forEach(o => {
|
||||||
|
g.rect(o.x, o.y, o.w, o.h).stroke({ width: 1, color: 0x00FFFF });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client (Grün)
|
||||||
|
obstacles.forEach(o => {
|
||||||
|
const def = o.def || {};
|
||||||
|
const rX = (o.prevX !== undefined) ? lerp(o.prevX, o.x, alpha) : o.x;
|
||||||
|
g.rect(rX, o.y, def.width||30, def.height||30).stroke({ width: 1, color: 0x00FF00 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: Erstellt eine Pixi-Sprechblase
|
||||||
|
function createSpeechBubble(text) {
|
||||||
|
const container = new PIXI.Container();
|
||||||
|
|
||||||
|
// 1. Text erstellen
|
||||||
|
const style = new PIXI.TextStyle({
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fill: '#000000',
|
||||||
|
align: 'center'
|
||||||
|
});
|
||||||
|
|
||||||
|
const pixiText = new PIXI.Text({ text: text, style: style });
|
||||||
|
pixiText.anchor.set(0.5); // Text-Mitte ist Anker
|
||||||
|
|
||||||
|
// Maße berechnen
|
||||||
|
const w = pixiText.width + 10;
|
||||||
|
const h = pixiText.height + 6;
|
||||||
|
|
||||||
|
// 2. Hintergrund (Blase)
|
||||||
|
const g = new PIXI.Graphics();
|
||||||
|
g.rect(-w/2, -h/2, w, h).fill(0xFFFFFF); // Weißer Kasten
|
||||||
|
g.rect(-w/2, -h/2, w, h).stroke({ width: 2, color: 0x000000 }); // Schwarzer Rand
|
||||||
|
|
||||||
|
// Kleines Dreieck unten (optional, für den "Speech"-Look)
|
||||||
|
g.moveTo(-5, h/2).lineTo(0, h/2 + 5).lineTo(5, h/2).fill(0xFFFFFF);
|
||||||
|
|
||||||
|
// Zusammenfügen
|
||||||
|
container.addChild(g);
|
||||||
|
container.addChild(pixiText);
|
||||||
|
|
||||||
|
// Name setzen, damit wir es später wiederfinden
|
||||||
|
container.label = "bubble";
|
||||||
|
|
||||||
|
return container;
|
||||||
}
|
}
|
||||||
@@ -1,35 +1,59 @@
|
|||||||
|
// ==========================================
|
||||||
|
// GLOBALE STATUS VARIABLEN
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
let gameConfig = null;
|
// --- KONFIGURATION & FLAGS ---
|
||||||
let isLoaded = false;
|
let gameConfig = null; // Wird von /api/config geladen
|
||||||
let isGameRunning = false;
|
let isLoaded = false; // Sind Assets geladen?
|
||||||
let isGameOver = false;
|
let isGameRunning = false; // Läuft der Game Loop?
|
||||||
let sessionID = null;
|
let isGameOver = false; // Ist der Spieler tot?
|
||||||
|
let sessionID = null; // UUID der aktuellen Session (vom Server)
|
||||||
|
|
||||||
let socket = null;
|
// --- NETZWERK & STREAMING ---
|
||||||
let obstacleBuffer = [];
|
let socket = null; // WebSocket Verbindung
|
||||||
let platformBuffer = [];
|
let obstacleBuffer = []; // Warteschlange Hindernisse (vom Server)
|
||||||
|
let platformBuffer = []; // Warteschlange Plattformen (vom Server)
|
||||||
|
|
||||||
let score = 0;
|
// --- LATENZ & SYNC ---
|
||||||
let currentTick = 0;
|
let currentLatencyMs = 0; // Gemessene One-Way Latenz
|
||||||
|
let pingInterval = null; // Interval ID für den Ping-Loop
|
||||||
|
|
||||||
|
// --- PIXI JS (RENDERING) ---
|
||||||
|
let app = null; // Die Pixi Application
|
||||||
|
let bgLayer = null; // Container: Hintergrund
|
||||||
|
let gameLayer = null; // Container: Spielwelt (Player, Items)
|
||||||
|
let debugLayer = null; // Graphics: Hitboxen
|
||||||
|
|
||||||
|
// --- CACHING (PIXI SPRITES) ---
|
||||||
|
// Map<LogikObjekt, PIXI.Sprite>
|
||||||
|
// Wir ordnen jedem Logik-Objekt ein festes Sprite zu, statt neu zu erstellen
|
||||||
|
const spriteCache = new Map();
|
||||||
|
const platformCache = new Map();
|
||||||
|
|
||||||
|
// Referenzen für statische Sprites
|
||||||
|
let pixiPlayer = null;
|
||||||
|
let bgSprite = null;
|
||||||
|
|
||||||
|
// --- SPIELZUSTAND ---
|
||||||
|
let score = 0; // Aktueller Score
|
||||||
|
let currentTick = 0; // Zeitbasis (Synchronisiert mit Server)
|
||||||
|
|
||||||
|
// --- POWERUPS (Client Visuals) ---
|
||||||
let godModeLives = 0;
|
let godModeLives = 0;
|
||||||
let hasBat = false;
|
let hasBat = false;
|
||||||
let bootTicks = 0;
|
let bootTicks = 0;
|
||||||
|
|
||||||
|
// --- HINTERGRUND LOGIK ---
|
||||||
|
let maxRawBgIndex = 0; // Welches BG Bild ist dran?
|
||||||
|
|
||||||
let maxRawBgIndex = 0;
|
// --- GAME LOOP TIMING ---
|
||||||
|
|
||||||
|
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
let accumulator = 0;
|
let accumulator = 0;
|
||||||
|
|
||||||
|
// --- GRAFIKEN & EFFEKTE ---
|
||||||
|
let particles = []; // Array für Partikel-Effekte
|
||||||
|
|
||||||
let sprites = {};
|
// --- ENTITIES (Render-Listen) ---
|
||||||
let playerSprite = new Image();
|
|
||||||
let bgSprites = [];
|
|
||||||
|
|
||||||
|
|
||||||
let player = {
|
let player = {
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 300,
|
y: 300,
|
||||||
@@ -38,30 +62,30 @@ let player = {
|
|||||||
color: "red",
|
color: "red",
|
||||||
vy: 0,
|
vy: 0,
|
||||||
grounded: false,
|
grounded: false,
|
||||||
|
|
||||||
|
// WICHTIG für Interpolation:
|
||||||
prevY: 300
|
prevY: 300
|
||||||
};
|
};
|
||||||
let particles = [];
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Diese Listen werden von logic.js gefüllt und von render.js gezeichnet
|
||||||
let obstacles = [];
|
let obstacles = [];
|
||||||
let platforms = [];
|
let platforms = [];
|
||||||
|
|
||||||
|
// Debug-Daten vom Server (für das Overlay)
|
||||||
let serverObstacles = [];
|
let serverObstacles = [];
|
||||||
let serverPlatforms = [];
|
let serverPlatforms = [];
|
||||||
|
|
||||||
let currentLatencyMs = 0;
|
// --- INPUT STATE ---
|
||||||
let pingInterval = null;
|
|
||||||
|
|
||||||
|
|
||||||
let isCrouching = false;
|
let isCrouching = false;
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
const canvas = document.getElementById('gameCanvas');
|
// HTML ELEMENTE (DOM Caching)
|
||||||
const ctx = canvas.getContext('2d');
|
// ==========================================
|
||||||
const container = document.getElementById('game-container');
|
const container = document.getElementById('game-container');
|
||||||
|
|
||||||
|
// Hinweis: 'canvas' und 'ctx' gibt es nicht mehr, da PixiJS das verwaltet!
|
||||||
|
|
||||||
|
// UI Elemente
|
||||||
const startScreen = document.getElementById('startScreen');
|
const startScreen = document.getElementById('startScreen');
|
||||||
const startBtn = document.getElementById('startBtn');
|
const startBtn = document.getElementById('startBtn');
|
||||||
const loadingText = document.getElementById('loadingText');
|
const loadingText = document.getElementById('loadingText');
|
||||||
|
|||||||
27
types.go
27
types.go
@@ -1,18 +1,27 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
// shared/types.go (oder types.go im Hauptverzeichnis)
|
||||||
|
|
||||||
type ObstacleDef struct {
|
type ObstacleDef struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Width float64 `json:"width"`
|
Width float64 `json:"width"`
|
||||||
Height float64 `json:"height"`
|
Height float64 `json:"height"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
Image string `json:"image"`
|
Image string `json:"image"`
|
||||||
|
|
||||||
|
// Visuelle Anpassungen
|
||||||
|
ImgScale float64 `json:"imgScale"`
|
||||||
|
ImgOffsetX float64 `json:"imgOffsetX"`
|
||||||
|
ImgOffsetY float64 `json:"imgOffsetY"`
|
||||||
|
|
||||||
|
// Logik
|
||||||
CanTalk bool `json:"canTalk"`
|
CanTalk bool `json:"canTalk"`
|
||||||
SpeechLines []string `json:"speechLines"`
|
SpeechLines []string `json:"speechLines"`
|
||||||
YOffset float64 `json:"yOffset"`
|
YOffset float64 `json:"yOffset"`
|
||||||
ImgScale float64 `json:"imgScale"`
|
|
||||||
ImgOffsetX float64 `json:"imgOffsetX"`
|
// NEU: Verhindert, dass dieses Objekt vom Zufallsgenerator ausgewählt wird
|
||||||
ImgOffsetY float64 `json:"imgOffsetY"`
|
NoRandomSpawn bool `json:"noRandomSpawn"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChunkObstacle struct {
|
type ChunkObstacle struct {
|
||||||
|
|||||||
@@ -288,6 +288,11 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac
|
|||||||
var pool []ObstacleDef
|
var pool []ObstacleDef
|
||||||
|
|
||||||
for _, d := range defs {
|
for _, d := range defs {
|
||||||
|
|
||||||
|
if d.NoRandomSpawn {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if isBoss {
|
if isBoss {
|
||||||
if d.ID == "principal" || d.ID == "trashcan" {
|
if d.ID == "principal" || d.ID == "trashcan" {
|
||||||
pool = append(pool, d)
|
pool = append(pool, d)
|
||||||
|
|||||||
Reference in New Issue
Block a user