Private
Public Access
1
0

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

Reviewed-on: #20
This commit was merged in pull request #20.
This commit is contained in:
2025-12-05 21:09:07 +00:00
20 changed files with 6209 additions and 835 deletions

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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()
} }

View File

@@ -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)
}

View File

@@ -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
View File

@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: game-assets-pvc
spec:
accessModes:
- ReadWriteMany
storageClassName: longhorn
resources:
requests:
storage: 2Gi

View File

@@ -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
View File

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

View File

@@ -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

File diff suppressed because one or more lines are too long

2312
static/assets/pixi.min.mjs Normal file

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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);
}
}
});

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;
} }

View File

@@ -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');

View File

@@ -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 {

View File

@@ -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)