fix README, SYNC, DATENSCHUTZ
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Dynamic Branch Deploy / build-and-deploy (push) Has been cancelled
This commit is contained in:
124
README.md
124
README.md
@@ -4,15 +4,18 @@
|
|||||||
|
|
||||||
## **📖 About the Project**
|
## **📖 About the Project**
|
||||||
|
|
||||||
"Escape the Teacher" is a web game developed as a school project. You play as a student caught cheating, running away from an angry teacher. The game features increasing difficulty, power-ups, boss phases, and a competitive global leaderboard.
|
"Escape the Teacher" is a web-based game developed as a final project (IT232). You play as a student caught cheating, running away from an angry teacher. The game features increasing difficulty, power-ups, boss phases, and a competitive global leaderboard.
|
||||||
|
|
||||||
Unlike typical browser games, this project implements **competitive multiplayer architecture** usually found in games like *Agar.io* or FPS titles. The browser is just a "dumb" terminal; the server simulates the entire world.
|
Unlike typical browser games, this project implements **competitive multiplayer architecture** usually found in shooters or RTS games. The browser is treated as a "dumb" display terminal; the server simulates the entire world to prevent cheating.
|
||||||
|
|
||||||
|
**🔗 Repository:** [https://git.zb-server.de/ZB-Server/it232Abschied](https://git.zb-server.de/ZB-Server/it232Abschied)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## **✨ Features**
|
## **✨ Features**
|
||||||
|
|
||||||
### **🎮 Gameplay**
|
### **🎮 Gameplay**
|
||||||
|
* **Endless Progression:** The game speed increases over time.
|
||||||
* **Endless Progression:** The game speeds up over time.
|
|
||||||
* **Controls:**
|
* **Controls:**
|
||||||
* **Jump:** Space / Arrow Up / Tap / Left Click.
|
* **Jump:** Space / Arrow Up / Tap / Left Click.
|
||||||
* **Crouch:** Arrow Down / Swipe Down (Mobile).
|
* **Crouch:** Arrow Down / Swipe Down (Mobile).
|
||||||
@@ -20,56 +23,54 @@ Unlike typical browser games, this project implements **competitive multiplayer
|
|||||||
* 🛡️ **Godmode:** Survives 3 hits.
|
* 🛡️ **Godmode:** Survives 3 hits.
|
||||||
* ⚾ **Baseball Bat:** Eliminates the next teacher obstacle.
|
* ⚾ **Baseball Bat:** Eliminates the next teacher obstacle.
|
||||||
* 👟 **Jumpboots:** Grants higher jumping power.
|
* 👟 **Jumpboots:** Grants higher jumping power.
|
||||||
* 💰 **Coins:** Bonus points.
|
* 💰 **Coins:** Bonus points for the highscore.
|
||||||
* **Level Editor:** Custom chunks (platforms, enemies) can be designed in a secure Admin UI and are streamed into the game live.
|
* **Level Editor:** Custom level segments ("Chunks") can be designed live in the Admin Panel and are streamed directly into active game sessions.
|
||||||
* **Juice:** Particle effects and retro sound system.
|
|
||||||
|
|
||||||
### **🛡️ Security & Admin**
|
### **🛡️ Security & Technology**
|
||||||
|
* **Server-Authoritative:** Physics runs entirely on the server (Go). Speedhacks or Godmode cheats are impossible.
|
||||||
|
* **Admin Panel:** Password-protected interface to manage levels, badwords, and leaderboards.
|
||||||
|
* **Proof System:** Players receive an 8-character "Claim Code" to validate their high score offline.
|
||||||
|
|
||||||
* **Server-Authoritative:** Physics runs on the server. Cheating (speedhack, godmode) is impossible.
|
---
|
||||||
* **Admin Panel:** Create levels, manage badwords, and moderate the leaderboard.
|
|
||||||
* **Proof System:** Players receive an 8-character "Claim Code" to prove their high score offline.
|
|
||||||
|
|
||||||
## **🏗️ Technical Architecture**
|
## **🏗️ Technical Architecture**
|
||||||
|
|
||||||
We moved from a traditional HTTP-Request model to a **Realtime Streaming Architecture**.
|
The project utilizes a **Realtime Streaming Architecture** to handle latency and synchronization.
|
||||||
|
|
||||||
1. **Backend (Go):**
|
1. **Backend (Go):**
|
||||||
* Runs the physics simulation at a fixed **20 TPS (Ticks Per Second)**.
|
* Simulates physics at a fixed **20 TPS (Ticks Per Second)** to save CPU.
|
||||||
* Generates level segments ("Chunks") 5 seconds into the future.
|
* Generates level chunks 5 seconds into the future.
|
||||||
* Streams object positions via **WebSockets** to the client.
|
* Streams object positions and game state via **WebSockets**.
|
||||||
2. **Frontend (JS):**
|
2. **Frontend (JS):**
|
||||||
* **Client-Side Prediction:** Inputs are applied immediately for zero-latency feel.
|
* **Client-Side Prediction:** Inputs are applied immediately for a "Zero-Lag" feel.
|
||||||
* **Buffering:** Incoming server data is buffered and played back smoothly.
|
* **Buffering & Interpolation:** Although the server calculates at 20 FPS, the client renders at 60+ FPS by interpolating positions (`Lerp`) between server ticks.
|
||||||
* **Interpolation:** Although physics runs at 20 FPS, the game renders at 60+ FPS by interpolating positions (`lerp`).
|
* **Lag Compensation (RTT):** The client measures Round Trip Time and visually shifts server objects to match the client's timeline.
|
||||||
3. **Database (Redis):**
|
3. **Database (Redis):**
|
||||||
* Stores active sessions, highscores, and custom level chunks.
|
* Stores highscores, active session states, and level editor chunks.
|
||||||
|
|
||||||
### **Tech Stack**
|
---
|
||||||
|
|
||||||
* **Backend:** Go (Golang) 1.22+ (`gorilla/websocket`)
|
|
||||||
* **Frontend:** Vanilla JavaScript (Canvas API)
|
|
||||||
* **Database:** Redis
|
|
||||||
* **Containerization:** Docker (Multi-Stage Build)
|
|
||||||
* **Orchestration:** Kubernetes
|
|
||||||
|
|
||||||
## **🔧 Engineering Challenges & Solutions**
|
## **🔧 Engineering Challenges & Solutions**
|
||||||
|
|
||||||
### **1. The "Netflix" Approach (Streaming vs. RNG)**
|
Building a lag-free, cheat-proof game on the web came with significant hurdles. Here is how we solved them:
|
||||||
* **Problem:** Syncing Random Number Generators (RNG) between Client and Server caused "Butterfly Effects" where one wrong number broke the whole game state.
|
|
||||||
* **Solution:** **Streaming.** The client no longer generates anything. The server generates objects in the future and streams them into a buffer on the client. The client simply plays back what it receives.
|
|
||||||
|
|
||||||
### **2. Lag Compensation & RTT**
|
### **1. The "Netflix" Approach (Streaming vs. RNG Sync)**
|
||||||
* **Problem:** If the internet lags, the server's objects appear too late on the client, causing "Ghost Kills".
|
* **Problem:** Initially, we tried to sync the Random Number Generators (RNG) between Client and Server. However, a single floating-point deviation caused a "Butterfly Effect," desynchronizing the entire game world.
|
||||||
* **Solution:** **RTT (Round Trip Time) Measurement.** The client constantly measures the Ping. Incoming objects are visually shifted based on latency (`Latency * Speed`), so they appear exactly where they are on the server.
|
* **Solution:** **Streaming.** The client no longer generates obstacles. The server pre-calculates the future and streams objects into a buffer on the client. The client simply plays back the stream, similar to a video player.
|
||||||
|
|
||||||
### **3. Low Tick-Rate & Interpolation**
|
### **2. Low Tick-Rate & Interpolation**
|
||||||
* **Problem:** To save server CPU, we run physics at only **20 TPS**. This usually looks choppy (like a slideshow).
|
* **Problem:** To allow many concurrent players, the server physics runs at only **20 TPS**. Without smoothing, this looks choppy (like a slideshow) on a 144Hz monitor.
|
||||||
* **Solution:** **Linear Interpolation.** The rendering loop runs at 60/144 FPS and calculates the visual position between two physics ticks. The game looks buttery smooth despite low server load.
|
* **Solution:** **Linear Interpolation.** The rendering engine calculates the visual position between the current and the previous physics state (`alpha = accumulator / dt`). The result is buttery smooth motion despite the low server update rate.
|
||||||
|
|
||||||
### **4. "Instant" Death**
|
### **3. Latency & "Ghost Kills"**
|
||||||
* **Problem:** Waiting for the server to confirm "You died" feels laggy.
|
* **Problem:** If the internet lags (e.g., 100ms Ping), the server sees the player hitting an obstacle that appeared "safe" on the client screen.
|
||||||
* **Solution:** **Optimistic Client Death.** The client detects collisions locally and stops the game visually (`Game Over`). It sends a `DEATH` signal to the server, which then validates and saves the highscore.
|
* **Solution:** **RTT Compensation.** The client constantly measures the network delay. Incoming objects from the server are visually shifted towards the player (`Latency * Speed`), ensuring they appear exactly where the server expects them to be at the moment of interaction.
|
||||||
|
|
||||||
|
### **4. Vertical Synchronization (Platforms)**
|
||||||
|
* **Problem:** Jumping onto platforms is tricky. At high speeds, the server might calculate that the player fell *through* a platform between two ticks ("Tunneling").
|
||||||
|
* **Solution:** **Vertical Sweeping & Client Authority override.** The server checks the *path* of the player, not just the position. Additionally, if the client detects a safe landing, it forces a physics sync update to the server to prevent unfair deaths.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## **🚀 Getting Started**
|
## **🚀 Getting Started**
|
||||||
|
|
||||||
@@ -100,33 +101,30 @@ We moved from a traditional HTTP-Request model to a **Realtime Streaming Archite
|
|||||||
go run .
|
go run .
|
||||||
```
|
```
|
||||||
|
|
||||||
## **📂 Project Structure**
|
---
|
||||||
|
|
||||||
```
|
## **🎶 Credits**
|
||||||
.
|
|
||||||
├── k8s/ \# Kubernetes manifests
|
|
||||||
├── static/ \# Frontend files
|
|
||||||
│ ├── assets/ \# Images & Audio
|
|
||||||
│ ├── js/ \# Game Engine
|
|
||||||
│ │ ├── audio.js \# Sound Manager
|
|
||||||
│ │ ├── logic.js \# Physics & Buffer Logic
|
|
||||||
│ │ ├── render.js \# Drawing & Interpolation
|
|
||||||
│ │ ├── network.js \# WebSocket & RTT Sync
|
|
||||||
│ │ └── ...
|
|
||||||
│ ├── index.html \# Entry Point
|
|
||||||
│ └── style.css \# Styling
|
|
||||||
├── secure/ \# Protected Admin Files (Editor)
|
|
||||||
├── main.go \# HTTP Routes & Setup
|
|
||||||
├── websocket.go \# Game Loop & Streaming Logic
|
|
||||||
├── simulation.go \# Physics Core
|
|
||||||
├── types.go \# Data Structures
|
|
||||||
└── Dockerfile \# Multi-Stage Build
|
|
||||||
```
|
|
||||||
|
|
||||||
## **📜 Legal**
|
This project was made possible by:
|
||||||
|
|
||||||
This is a non-commercial educational project.
|
* **Development & Code:** Sebastian Unterschütz
|
||||||
* **Privacy:** No tracking cookies.
|
* **Music Design:** Max E.
|
||||||
* **Assets:** Font "Press Start 2P" hosted locally. Sounds generated via bfxr.net.
|
* **Sound Effects:** Generated via [bfxr.net](https://www.bfxr.net/)
|
||||||
|
* **Graphics:** [Kenney.nl](https://kenney.nl) / Sebastian Unterschütz
|
||||||
|
* **Fonts:** "Press Start 2P" (Google Fonts / Local)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## **⚖️ License & Rights**
|
||||||
|
|
||||||
|
**© 2025 IT232 Final Project**
|
||||||
|
|
||||||
|
This project is released under a proprietary, restrictive license:
|
||||||
|
|
||||||
|
1. **No Commercial Use:** The source code, assets, and the game itself may not be used for commercial purposes or sold.
|
||||||
|
2. **No Modification:** You are not allowed to modify, fork, or redistribute this project in modified form without express written permission from the authors.
|
||||||
|
3. **Educational Use:** Viewing and running the code for educational purposes within the context of the school project is permitted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**Run for your grade! 🏃💨**
|
**Run for your grade! 🏃💨**
|
||||||
@@ -21,7 +21,6 @@ const (
|
|||||||
GameWidth = 800.0
|
GameWidth = 800.0
|
||||||
)
|
)
|
||||||
|
|
||||||
// Globale Variablen
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
rdb *redis.Client
|
rdb *redis.Client
|
||||||
@@ -58,7 +57,7 @@ func initGameConfig() {
|
|||||||
{ID: "p_bat", Type: "powerup", Width: 30, Height: 30, Color: "red", Image: "powerup_bat1.png", YOffset: 20.0}, // Schläger
|
{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
|
{ID: "p_boot", Type: "powerup", Width: 30, Height: 30, Color: "lime", Image: "powerup_boot1.png", YOffset: 20.0}, // Boots
|
||||||
},
|
},
|
||||||
// Mehrere Hintergründe für Level-Wechsel
|
|
||||||
Backgrounds: []string{"school-background.jpg", "gym-background.jpg", "school2-background.jpg"},
|
Backgrounds: []string{"school-background.jpg", "gym-background.jpg", "school2-background.jpg"},
|
||||||
}
|
}
|
||||||
log.Println("✅ Config mit Powerups geladen")
|
log.Println("✅ Config mit Powerups geladen")
|
||||||
@@ -90,7 +89,6 @@ func loadChunksFromRedis() []ChunkDef {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log nur beim Server-Start (wenn defaultConfig leer ist), sonst spammt es
|
|
||||||
if len(defaultConfig.Chunks) == 0 {
|
if len(defaultConfig.Chunks) == 0 {
|
||||||
log.Printf("📦 Lade %d Chunks aus Redis", len(loadedChunks))
|
log.Printf("📦 Lade %d Chunks aus Redis", len(loadedChunks))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,6 @@ func handleSubmitName(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validierung
|
|
||||||
if len(req.Name) > 4 {
|
if len(req.Name) > 4 {
|
||||||
http.Error(w, "Zu lang", 400)
|
http.Error(w, "Zu lang", 400)
|
||||||
return
|
return
|
||||||
@@ -297,7 +296,6 @@ func handleClaimDelete(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
log.Printf("🗑️ USER DELETE: Session %s deleted via code", req.SessionID)
|
log.Printf("🗑️ USER DELETE: Session %s deleted via code", req.SessionID)
|
||||||
|
|
||||||
// Aus Listen entfernen
|
|
||||||
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
|
rdb.ZRem(ctx, "leaderboard:unverified", req.SessionID)
|
||||||
rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
|
rdb.ZRem(ctx, "leaderboard:public", req.SessionID)
|
||||||
|
|
||||||
@@ -318,7 +316,6 @@ func generateClaimCode() string {
|
|||||||
func handleAdminBadwords(w http.ResponseWriter, r *http.Request) {
|
func handleAdminBadwords(w http.ResponseWriter, r *http.Request) {
|
||||||
key := "config:badwords"
|
key := "config:badwords"
|
||||||
|
|
||||||
// GET: Liste abrufen
|
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
words, _ := rdb.SMembers(ctx, key).Result()
|
words, _ := rdb.SMembers(ctx, key).Result()
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
@@ -326,12 +323,11 @@ func handleAdminBadwords(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: Hinzufügen oder Löschen
|
|
||||||
if r.Method == http.MethodPost {
|
if r.Method == http.MethodPost {
|
||||||
// Wir nutzen ein einfaches Struct für den Request
|
|
||||||
type WordReq struct {
|
type WordReq struct {
|
||||||
Word string `json:"word"`
|
Word string `json:"word"`
|
||||||
Action string `json:"action"` // "add" oder "remove"
|
Action string `json:"action"`
|
||||||
}
|
}
|
||||||
var req WordReq
|
var req WordReq
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
|||||||
2
main.go
2
main.go
@@ -35,7 +35,6 @@ func main() {
|
|||||||
fs := http.FileServer(http.Dir("./static"))
|
fs := http.FileServer(http.Dir("./static"))
|
||||||
http.Handle("/", fs)
|
http.Handle("/", fs)
|
||||||
|
|
||||||
// API Routes (jetzt mit Logger!)
|
|
||||||
http.HandleFunc("/api/config", Logger(handleConfig))
|
http.HandleFunc("/api/config", Logger(handleConfig))
|
||||||
http.HandleFunc("/api/start", Logger(handleStart))
|
http.HandleFunc("/api/start", Logger(handleStart))
|
||||||
http.HandleFunc("/api/validate", Logger(handleValidate))
|
http.HandleFunc("/api/validate", Logger(handleValidate))
|
||||||
@@ -44,7 +43,6 @@ func main() {
|
|||||||
http.HandleFunc("/api/leaderboard", Logger(handleLeaderboard))
|
http.HandleFunc("/api/leaderboard", Logger(handleLeaderboard))
|
||||||
http.HandleFunc("/api/claim/delete", Logger(handleClaimDelete))
|
http.HandleFunc("/api/claim/delete", Logger(handleClaimDelete))
|
||||||
|
|
||||||
// Admin Routes (Logger + BasicAuth kombinieren)
|
|
||||||
http.HandleFunc("/admin", Logger(BasicAuth(handleAdminPage)))
|
http.HandleFunc("/admin", Logger(BasicAuth(handleAdminPage)))
|
||||||
http.HandleFunc("/api/admin/badwords", Logger(BasicAuth(handleAdminBadwords)))
|
http.HandleFunc("/api/admin/badwords", Logger(BasicAuth(handleAdminBadwords)))
|
||||||
http.HandleFunc("/api/admin/list", Logger(BasicAuth(handleAdminList)))
|
http.HandleFunc("/api/admin/list", Logger(BasicAuth(handleAdminList)))
|
||||||
|
|||||||
@@ -6,40 +6,29 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wir bauen unseren eigenen ResponseWriter, der den Status-Code "mitschreibt"
|
|
||||||
type StatusRecorder struct {
|
type StatusRecorder struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
Status int
|
Status int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Überschreiben der WriteHeader Methode, um den Code abzufangen
|
|
||||||
func (r *StatusRecorder) WriteHeader(status int) {
|
func (r *StatusRecorder) WriteHeader(status int) {
|
||||||
r.Status = status
|
r.Status = status
|
||||||
r.ResponseWriter.WriteHeader(status)
|
r.ResponseWriter.WriteHeader(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Die eigentliche Middleware Funktion
|
|
||||||
func Logger(next http.HandlerFunc) http.HandlerFunc {
|
func Logger(next http.HandlerFunc) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
// 1. Startzeit messen
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
// 2. Recorder initialisieren (Standard ist 200 OK)
|
|
||||||
recorder := &StatusRecorder{
|
recorder := &StatusRecorder{
|
||||||
ResponseWriter: w,
|
ResponseWriter: w,
|
||||||
Status: http.StatusOK,
|
Status: http.StatusOK,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Den echten Handler aufrufen (mit unserem Recorder)
|
|
||||||
next(recorder, r)
|
next(recorder, r)
|
||||||
|
|
||||||
// 4. Dauer berechnen
|
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
// 5. Loggen
|
|
||||||
// Format: [METHODE] PFAD | STATUS | DAUER | IP
|
|
||||||
// Beispiel: [POST] /api/validate | 200 | 1.2ms | 127.0.0.1
|
|
||||||
|
|
||||||
icon := "✅"
|
icon := "✅"
|
||||||
if recorder.Status >= 400 {
|
if recorder.Status >= 400 {
|
||||||
icon = "⚠️"
|
icon = "⚠️"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- INTERNE STATE STRUKTUR ---
|
|
||||||
type SimState struct {
|
type SimState struct {
|
||||||
SessionID string
|
SessionID string
|
||||||
Score int
|
Score int
|
||||||
@@ -37,16 +36,10 @@ type SimState struct {
|
|||||||
Chunks []ChunkDef
|
Chunks []ChunkDef
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HAUPTFUNKTION
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle, []ActivePlatform, PowerUpState, int, int, uint32) {
|
func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[string]string) (bool, int, []ActiveObstacle, []ActivePlatform, PowerUpState, int, int, uint32) {
|
||||||
|
|
||||||
// 1. State laden
|
|
||||||
state := loadSimState(sessionID, vals)
|
state := loadSimState(sessionID, vals)
|
||||||
|
|
||||||
// 2. Bot-Check
|
|
||||||
if isBotSpamming(inputs) {
|
if isBotSpamming(inputs) {
|
||||||
log.Printf("🤖 BOT-ALARM [%s]: Spammt Sprünge", sessionID)
|
log.Printf("🤖 BOT-ALARM [%s]: Spammt Sprünge", sessionID)
|
||||||
state.IsDead = true
|
state.IsDead = true
|
||||||
@@ -73,42 +66,32 @@ func simulateChunk(sessionID string, inputs []Input, totalTicks int, vals map[st
|
|||||||
state.Score++
|
state.Score++
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Anti-Cheat Heuristik
|
|
||||||
if state.SuspicionScore > 15 {
|
if state.SuspicionScore > 15 {
|
||||||
log.Printf("🤖 BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik)", sessionID)
|
log.Printf("🤖 BOT-ALARM [%s]: Zu perfekte Sprünge (Heuristik)", sessionID)
|
||||||
state.IsDead = true
|
state.IsDead = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Speichern
|
|
||||||
saveSimState(&state)
|
saveSimState(&state)
|
||||||
return packResponse(&state)
|
return packResponse(&state)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// LOGIK & PHYSIK FUNKTIONEN
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
func updatePhysics(s *SimState, didJump, isCrouching bool, currentSpeed float64) {
|
func updatePhysics(s *SimState, didJump, isCrouching bool, currentSpeed float64) {
|
||||||
// 1. Powerup Logik (Jump Boots)
|
|
||||||
jumpPower := JumpPower
|
jumpPower := JumpPower
|
||||||
if s.BootTicks > 0 {
|
if s.BootTicks > 0 {
|
||||||
jumpPower = HighJumpPower
|
jumpPower = HighJumpPower
|
||||||
s.BootTicks--
|
s.BootTicks--
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Sind wir am Boden?
|
|
||||||
isGrounded := checkGrounded(s)
|
isGrounded := checkGrounded(s)
|
||||||
|
|
||||||
// 3. Ducken / Fast Fall
|
|
||||||
// (Variable 'currentHeight' entfernt, da sie hier nicht gebraucht wird)
|
|
||||||
if isCrouching {
|
if isCrouching {
|
||||||
// Wenn man in der Luft duckt, fällt man schneller ("Fast Fall")
|
|
||||||
if !isGrounded {
|
if !isGrounded {
|
||||||
s.VelY += 2.0
|
s.VelY += 2.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Springen
|
|
||||||
if didJump && isGrounded && !isCrouching {
|
if didJump && isGrounded && !isCrouching {
|
||||||
s.VelY = jumpPower
|
s.VelY = jumpPower
|
||||||
isGrounded = false
|
isGrounded = false
|
||||||
@@ -122,47 +105,30 @@ func updatePhysics(s *SimState, didJump, isCrouching bool, currentSpeed float64)
|
|||||||
|
|
||||||
landed := false
|
landed := false
|
||||||
|
|
||||||
// ============================================================
|
if s.VelY > 0 {
|
||||||
// PLATTFORM KOLLISION (MIT VERTICAL SWEEP)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
if s.VelY > 0 { // Nur wenn wir fallen
|
|
||||||
|
|
||||||
// Wir nutzen hier die Standard-Höhe für die Füße.
|
|
||||||
// Auch beim Ducken bleiben die Füße meist unten (oder ziehen hoch?),
|
|
||||||
// aber für die Landung auf Plattformen ist die Standard-Box sicherer.
|
|
||||||
playerFeetOld := oldY + PlayerHeight
|
playerFeetOld := oldY + PlayerHeight
|
||||||
playerFeetNew := newY + PlayerHeight
|
playerFeetNew := newY + PlayerHeight
|
||||||
|
|
||||||
// Player X ist fest bei 50, Breite 30
|
|
||||||
pLeft := 50.0
|
pLeft := 50.0
|
||||||
pRight := 50.0 + 30.0
|
pRight := 50.0 + 30.0
|
||||||
|
|
||||||
for _, p := range s.Platforms {
|
for _, p := range s.Platforms {
|
||||||
|
|
||||||
// 1. Horizontal Check (Großzügig!)
|
|
||||||
// Toleranz an den Rändern (-5 / +5), damit man nicht abrutscht
|
|
||||||
if (pRight-5.0 > p.X) && (pLeft+5.0 < p.X+p.Width) {
|
if (pRight-5.0 > p.X) && (pLeft+5.0 < p.X+p.Width) {
|
||||||
|
|
||||||
// 2. Vertikaler Sweep (Durchsprung-Schutz)
|
|
||||||
// Check: Füße waren vorher <= Plattform-Oberkante
|
|
||||||
// UND Füße sind jetzt >= Plattform-Oberkante
|
|
||||||
if playerFeetOld <= p.Y && playerFeetNew >= p.Y {
|
if playerFeetOld <= p.Y && playerFeetNew >= p.Y {
|
||||||
|
|
||||||
// Korrektur: Wir setzen den Spieler exakt AUF die Plattform
|
|
||||||
newY = p.Y - PlayerHeight
|
newY = p.Y - PlayerHeight
|
||||||
s.VelY = 0
|
s.VelY = 0
|
||||||
landed = true
|
landed = true
|
||||||
isGrounded = true
|
isGrounded = true
|
||||||
break // Landung erfolgreich
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// BODEN KOLLISION
|
|
||||||
// ============================================================
|
|
||||||
if !landed {
|
if !landed {
|
||||||
if newY >= PlayerYBase {
|
if newY >= PlayerYBase {
|
||||||
newY = PlayerYBase
|
newY = PlayerYBase
|
||||||
@@ -171,7 +137,6 @@ func updatePhysics(s *SimState, didJump, isCrouching bool, currentSpeed float64)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Neue Position setzen
|
|
||||||
s.PosY = newY
|
s.PosY = newY
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +151,7 @@ func checkCollisions(s *SimState, isCrouching bool, currentSpeed float64) {
|
|||||||
activeObs := []ActiveObstacle{}
|
activeObs := []ActiveObstacle{}
|
||||||
|
|
||||||
for _, obs := range s.Obstacles {
|
for _, obs := range s.Obstacles {
|
||||||
// Passed Check
|
|
||||||
paddingX := 10.0
|
paddingX := 10.0
|
||||||
if obs.X+obs.Width-paddingX < 55.0 {
|
if obs.X+obs.Width-paddingX < 55.0 {
|
||||||
activeObs = append(activeObs, obs)
|
activeObs = append(activeObs, obs)
|
||||||
@@ -286,21 +251,19 @@ func handleSpawning(s *SimState, speed float64) {
|
|||||||
if s.Ticks >= s.NextSpawnTick {
|
if s.Ticks >= s.NextSpawnTick {
|
||||||
spawnX := GameWidth + 3200.0
|
spawnX := GameWidth + 3200.0
|
||||||
|
|
||||||
// --- OPTION A: CUSTOM CHUNK (20% Chance) ---
|
|
||||||
chunkCount := len(defaultConfig.Chunks)
|
chunkCount := len(defaultConfig.Chunks)
|
||||||
if chunkCount > 0 && s.RNG.NextFloat() > 0.8 {
|
if chunkCount > 0 && s.RNG.NextFloat() > 0.8 {
|
||||||
|
|
||||||
idx := int(s.RNG.NextRange(0, float64(chunkCount)))
|
idx := int(s.RNG.NextRange(0, float64(chunkCount)))
|
||||||
chunk := defaultConfig.Chunks[idx]
|
chunk := defaultConfig.Chunks[idx]
|
||||||
|
|
||||||
// Objekte spawnen
|
|
||||||
for _, p := range chunk.Platforms {
|
for _, p := range chunk.Platforms {
|
||||||
s.Platforms = append(s.Platforms, ActivePlatform{
|
s.Platforms = append(s.Platforms, ActivePlatform{
|
||||||
X: spawnX + p.X, Y: p.Y, Width: p.Width, Height: p.Height,
|
X: spawnX + p.X, Y: p.Y, Width: p.Width, Height: p.Height,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for _, o := range chunk.Obstacles {
|
for _, o := range chunk.Obstacles {
|
||||||
// Fehler behoben: Zugriff auf o.X, o.Y jetzt möglich dank neuem Types-Struct
|
|
||||||
s.Obstacles = append(s.Obstacles, ActiveObstacle{
|
s.Obstacles = append(s.Obstacles, ActiveObstacle{
|
||||||
ID: o.ID, Type: o.Type, X: spawnX + o.X, Y: o.Y, Width: o.Width, Height: o.Height,
|
ID: o.ID, Type: o.Type, X: spawnX + o.X, Y: o.Y, Width: o.Width, Height: o.Height,
|
||||||
})
|
})
|
||||||
@@ -311,20 +274,14 @@ func handleSpawning(s *SimState, speed float64) {
|
|||||||
width = 2000
|
width = 2000
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fehler behoben: Mismatched Types (int vs float64)
|
|
||||||
s.NextSpawnTick = s.Ticks + int(float64(width)/speed)
|
s.NextSpawnTick = s.Ticks + int(float64(width)/speed)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// --- OPTION B: RANDOM GENERATION ---
|
|
||||||
spawnRandomObstacle(s, speed, spawnX)
|
spawnRandomObstacle(s, speed, spawnX)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// HELPER
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
func loadSimState(sid string, vals map[string]string) SimState {
|
func loadSimState(sid string, vals map[string]string) SimState {
|
||||||
rngState, _ := strconv.ParseInt(vals["rng_state"], 10, 64)
|
rngState, _ := strconv.ParseInt(vals["rng_state"], 10, 64)
|
||||||
|
|
||||||
|
|||||||
@@ -88,47 +88,91 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="modal-impressum" class="modal-overlay">
|
<div id="modal-impressum" 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>Impressum</h2>
|
<h2>Impressum & Credits</h2>
|
||||||
<p><strong>Angaben gemäß § 5 TMG:</strong></p>
|
|
||||||
<p>
|
<p><strong>Projektleitung & Code:</strong><br>
|
||||||
Sebastian Unterschütz<br>
|
Sebastian Unterschütz<br>
|
||||||
Göltzschtalblick 16 <br>
|
Göltzschtalblick 16<br>
|
||||||
08236 Ellefeld
|
08236 Ellefeld<br>
|
||||||
|
<small>Kontakt: sebastian@unterschutz.de</small>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr style="border:1px solid #444; margin: 15px 0;">
|
||||||
|
|
||||||
|
<p><strong>🎵 Musik & Sound Design:</strong><br>
|
||||||
|
<span style="color:#ffcc00; font-size:18px;">Max E.</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p><strong>💻 Quellcode:</strong><br>
|
||||||
|
<a href="https://git.zb-server.de/ZB-Server/it232Abschied" target="_blank" style="color:#2196F3; text-decoration:none;">
|
||||||
|
git.zb-server.de/ZB-Server/it232Abschied
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr style="border:1px solid #444; margin: 15px 0;">
|
||||||
|
|
||||||
|
<p><strong>⚖️ Lizenzhinweis:</strong></p>
|
||||||
|
<p style="font-size:12px; color:#aaa;">
|
||||||
|
Dies ist ein Schulprojekt. <br>
|
||||||
|
<strong>Kommerzielle Nutzung und Veränderung des Quellcodes sind ausdrücklich untersagt.</strong><br>
|
||||||
|
Alle Rechte liegen bei den Urhebern.
|
||||||
</p>
|
</p>
|
||||||
<p>Kontakt: sebastian@unterschuetz.de</p>
|
|
||||||
<p><em>Dies ist ein Schulprojekt ohne kommerzielle Absicht.</em></p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="modal-datenschutz" class="modal-overlay">
|
<div id="modal-datenschutz" 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>Datenschutz</h2>
|
<h2>Datenschutzerklärung</h2>
|
||||||
|
|
||||||
<p><strong>1. Allgemeines</strong><br>
|
<p><strong>1. Datenschutz auf einen Blick</strong><br>
|
||||||
Dies ist ein Schulprojekt. Wir speichern so wenig Daten wie möglich.</p>
|
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>
|
||||||
|
|
||||||
<p><strong>2. Welche Daten speichern wir?</strong><br>
|
<p><strong>2. Verantwortlicher</strong><br>
|
||||||
Wenn du einen Highscore einträgst, speichern wir auf unserem Server:
|
Verantwortlich für die Datenverarbeitung auf dieser Website ist:<br>
|
||||||
|
Sebastian Unterschütz<br>
|
||||||
|
Göltzschtalblick 16, 08236 Ellefeld<br>
|
||||||
|
E-Mail: sebastian@unterschutz.de<br>
|
||||||
|
<em>(Schulprojekt im Rahmen der IT232)</em></p>
|
||||||
|
|
||||||
|
<p><strong>3. Hosting (Hetzner)</strong><br>
|
||||||
|
Wir hosten die Inhalte unserer Website bei folgendem Anbieter:<br>
|
||||||
|
<strong>Hetzner Online GmbH</strong><br>
|
||||||
|
Industriestr. 25<br>
|
||||||
|
91710 Gunzenhausen<br>
|
||||||
|
Deutschland<br>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<p><strong>4. Datenerfassung auf dieser Website</strong></p>
|
||||||
|
|
||||||
|
<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:
|
||||||
<ul>
|
<ul>
|
||||||
<li>Deinen gewählten Namen</li>
|
<li>Uhrzeit der Serveranfrage</li>
|
||||||
<li>Deinen Punktestand</li>
|
<li>IP-Adresse</li>
|
||||||
<li>Einen Zeitstempel</li>
|
|
||||||
<li>Einen zufälligen "Beweis-Code"</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
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>3. Lokale Speicherung (Dein Gerät)</strong><br>
|
<p><strong>Spielstände & Highscores</strong><br>
|
||||||
Das Spiel nutzt den "LocalStorage" deines Browsers, um deinen persönlichen Rekord und deine gesammelten Beweis-Codes zu speichern. Diese Daten verlassen dein Gerät nicht, außer du sendest sie aktiv ab.</p>
|
Wenn Sie einen Highscore eintragen, speichern wir in unserer Datenbank:
|
||||||
|
<ul>
|
||||||
|
<li>Den von Ihnen gewählten Namen (Pseudonym empfohlen!)</li>
|
||||||
|
<li>Ihren Punktestand und Zeitstempel</li>
|
||||||
|
<li>Eine Session-ID und einen "Claim-Code" zur Verifizierung</li>
|
||||||
|
</ul>
|
||||||
|
Diese Daten dienen ausschließlich der Darstellung der Bestenliste und der Spielmechanik.</p>
|
||||||
|
|
||||||
<p><strong>4. Cookies & Tracking</strong><br>
|
<p><strong>Lokale Speicherung (LocalStorage)</strong><br>
|
||||||
Wir verwenden <strong>keine</strong> Tracking-Cookies, keine Analyse-Tools (wie Google Analytics) und laden keine Schriftarten von fremden Servern.</p>
|
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>
|
||||||
|
|
||||||
<p><strong>5. Deine Rechte</strong><br>
|
<p><strong>5. Ihre Rechte</strong><br>
|
||||||
Du kannst deine Einträge jederzeit selbstständig über das Menü "Meine Codes" vom Server löschen.</p>
|
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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ SOUNDS.hit.volume = 0.6;
|
|||||||
SOUNDS.music.loop = true;
|
SOUNDS.music.loop = true;
|
||||||
SOUNDS.music.volume = 0.2;
|
SOUNDS.music.volume = 0.2;
|
||||||
|
|
||||||
// --- STATUS LADEN ---
|
|
||||||
// Wir lesen den String 'true'/'false' aus dem LocalStorage
|
|
||||||
let isMuted = localStorage.getItem('escape_muted') === 'true';
|
let isMuted = localStorage.getItem('escape_muted') === 'true';
|
||||||
|
|
||||||
function playSound(name) {
|
function playSound(name) {
|
||||||
@@ -29,15 +28,13 @@ function playSound(name) {
|
|||||||
function toggleMute() {
|
function toggleMute() {
|
||||||
isMuted = !isMuted;
|
isMuted = !isMuted;
|
||||||
|
|
||||||
// --- STATUS SPEICHERN ---
|
|
||||||
localStorage.setItem('escape_muted', isMuted);
|
localStorage.setItem('escape_muted', isMuted);
|
||||||
|
|
||||||
// Musik sofort pausieren/starten
|
|
||||||
if(isMuted) {
|
if(isMuted) {
|
||||||
SOUNDS.music.pause();
|
SOUNDS.music.pause();
|
||||||
} else {
|
} else {
|
||||||
// Nur starten, wenn wir schon im Spiel sind (user interaction needed)
|
|
||||||
// Wir fangen Fehler ab, falls der Browser Autoplay blockiert
|
|
||||||
SOUNDS.music.play().catch(()=>{});
|
SOUNDS.music.play().catch(()=>{});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,13 +42,13 @@ function toggleMute() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startMusic() {
|
function startMusic() {
|
||||||
// Nur abspielen, wenn NICHT stummgeschaltet
|
|
||||||
if(!isMuted) {
|
if(!isMuted) {
|
||||||
SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked", e));
|
SOUNDS.music.play().catch(e => console.log("Audio Autoplay blocked", e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getter für UI
|
|
||||||
function getMuteState() {
|
function getMuteState() {
|
||||||
return isMuted;
|
return isMuted;
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,22 @@
|
|||||||
// ==========================================
|
|
||||||
// SPIEL KONFIGURATION & KONSTANTEN
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
// Dimensionen (Muss zum Canvas passen)
|
|
||||||
const GAME_WIDTH = 800;
|
const GAME_WIDTH = 800;
|
||||||
const GAME_HEIGHT = 400;
|
const GAME_HEIGHT = 400;
|
||||||
|
|
||||||
// Physik (Muss exakt synchron zum Go-Server sein!)
|
|
||||||
const GRAVITY = 1.8;
|
const GRAVITY = 1.8;
|
||||||
const JUMP_POWER = -20.0; // Vorher -36.0 (Deutlich weniger!)
|
const JUMP_POWER = -20.0;
|
||||||
const HIGH_JUMP_POWER = -28.0;// Vorher -48.0 (Boots)
|
const HIGH_JUMP_POWER = -28.0;
|
||||||
const GROUND_Y = 350; // Y-Position des Bodens
|
const GROUND_Y = 350;
|
||||||
|
|
||||||
|
|
||||||
// Geschwindigkeit
|
|
||||||
const BASE_SPEED = 15.0;
|
const BASE_SPEED = 15.0;
|
||||||
|
|
||||||
// Game Loop Einstellungen
|
|
||||||
const TARGET_FPS = 20;
|
const TARGET_FPS = 20;
|
||||||
const MS_PER_TICK = 1000 / TARGET_FPS;
|
const MS_PER_TICK = 1000 / TARGET_FPS;
|
||||||
const CHUNK_SIZE = 20; // Intervall für Berechnungen (Legacy)
|
const CHUNK_SIZE = 20;
|
||||||
|
|
||||||
|
|
||||||
// Debugging
|
|
||||||
// true = Zeigt Hitboxen (Grün) und Server-Daten (Cyan)
|
|
||||||
const DEBUG_SYNC = true;
|
const DEBUG_SYNC = true;
|
||||||
|
|
||||||
function lerp(a, b, t) {
|
function lerp(a, b, t) {
|
||||||
|
|||||||
@@ -3,41 +3,31 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
function handleInput(action, active) {
|
function handleInput(action, active) {
|
||||||
// 1. Game Over Reset
|
|
||||||
if (isGameOver) {
|
if (isGameOver) {
|
||||||
if(active) location.reload();
|
if(active) location.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. JUMP LOGIK
|
|
||||||
if (action === "JUMP" && active) {
|
if (action === "JUMP" && active) {
|
||||||
// Wir prüfen lokal, ob wir springen dürfen (Client Prediction)
|
|
||||||
if (player.grounded && !isCrouching) {
|
if (player.grounded && !isCrouching) {
|
||||||
|
|
||||||
// A. Sofort lokal anwenden (damit es sich direkt anfühlt)
|
|
||||||
player.vy = JUMP_POWER;
|
player.vy = JUMP_POWER;
|
||||||
player.grounded = false;
|
player.grounded = false;
|
||||||
|
|
||||||
playSound('jump');
|
playSound('jump');
|
||||||
spawnParticles(player.x + 15, player.y + 50, 'dust', 5); // Staubwolke an den Füßen
|
spawnParticles(player.x + 15, player.y + 50, 'dust', 5); // Staubwolke an den Füßen
|
||||||
|
|
||||||
// B. An Server senden ("Ich habe JETZT gedrückt")
|
|
||||||
// Die Funktion sendInput ist in network.js definiert
|
|
||||||
if (typeof sendInput === "function") {
|
if (typeof sendInput === "function") {
|
||||||
sendInput("input", "JUMP");
|
sendInput("input", "JUMP");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. DUCK LOGIK
|
|
||||||
if (action === "DUCK") {
|
if (action === "DUCK") {
|
||||||
// Status merken, um unnötiges Senden zu vermeiden
|
|
||||||
const wasCrouching = isCrouching;
|
const wasCrouching = isCrouching;
|
||||||
|
|
||||||
// A. Lokal anwenden
|
|
||||||
isCrouching = active;
|
isCrouching = active;
|
||||||
|
|
||||||
// B. An Server senden (State Change: Start oder Ende)
|
|
||||||
if (wasCrouching !== isCrouching) {
|
if (wasCrouching !== isCrouching) {
|
||||||
if (typeof sendInput === "function") {
|
if (typeof sendInput === "function") {
|
||||||
sendInput("input", active ? "DUCK_START" : "DUCK_END");
|
sendInput("input", active ? "DUCK_START" : "DUCK_END");
|
||||||
@@ -50,9 +40,9 @@ function handleInput(action, active) {
|
|||||||
// EVENT LISTENERS
|
// EVENT LISTENERS
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
// Tastatur
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
// Ignorieren, wenn User gerade Name in Highscore tippt
|
|
||||||
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);
|
||||||
@@ -61,7 +51,6 @@ window.addEventListener('keydown', (e) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
console.log("🐞 Fordere Debug-Daten vom Server an...");
|
console.log("🐞 Fordere Debug-Daten vom Server an...");
|
||||||
if (typeof sendInput === "function") {
|
if (typeof sendInput === "function") {
|
||||||
// Wir senden ein manuelles Paket, da sendInput meist nur für Game-Inputs ist
|
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
socket.send(JSON.stringify({ type: "debug" }));
|
socket.send(JSON.stringify({ type: "debug" }));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
function updateGameLogic() {
|
function updateGameLogic() {
|
||||||
// ===============================================
|
|
||||||
// 1. GESCHWINDIGKEIT
|
|
||||||
// ===============================================
|
|
||||||
// Wir nutzen den lokalen Score für die Geschwindigkeit
|
|
||||||
let currentSpeed = BASE_SPEED + (currentTick / 1000.0) * 1.5;
|
let currentSpeed = BASE_SPEED + (currentTick / 1000.0) * 1.5;
|
||||||
if (currentSpeed > 36.0) currentSpeed = 36.0;
|
if (currentSpeed > 36.0) currentSpeed = 36.0;
|
||||||
|
|
||||||
@@ -14,33 +10,25 @@ function updateGameLogic() {
|
|||||||
obstacleBuffer.forEach(o => o.prevX = o.x);
|
obstacleBuffer.forEach(o => o.prevX = o.x);
|
||||||
platformBuffer.forEach(p => p.prevX = p.x);
|
platformBuffer.forEach(p => p.prevX = p.x);
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// 2. SPIELER PHYSIK (CLIENT PREDICTION)
|
|
||||||
// ===============================================
|
|
||||||
const originalHeight = 50;
|
const originalHeight = 50;
|
||||||
const crouchHeight = 25;
|
const crouchHeight = 25;
|
||||||
|
|
||||||
// Hitbox & Y-Pos anpassen
|
|
||||||
player.h = isCrouching ? crouchHeight : originalHeight;
|
player.h = isCrouching ? crouchHeight : originalHeight;
|
||||||
let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y;
|
let drawY = isCrouching ? player.y + (originalHeight - crouchHeight) : player.y;
|
||||||
|
|
||||||
// Alte Position (für One-Way Check)
|
|
||||||
const oldY = player.y;
|
const oldY = player.y;
|
||||||
|
|
||||||
// Physik
|
|
||||||
player.vy += GRAVITY;
|
player.vy += GRAVITY;
|
||||||
if (isCrouching && !player.grounded) player.vy += 2.0;
|
if (isCrouching && !player.grounded) player.vy += 2.0;
|
||||||
|
|
||||||
let newY = player.y + player.vy;
|
let newY = player.y + player.vy;
|
||||||
let landed = false;
|
let landed = false;
|
||||||
|
|
||||||
// --- PLATTFORMEN ---
|
|
||||||
if (player.vy > 0) {
|
if (player.vy > 0) {
|
||||||
for (let plat of platformBuffer) {
|
for (let plat of platformBuffer) {
|
||||||
// Nur relevante Plattformen prüfen
|
|
||||||
if (plat.x < GAME_WIDTH + 100 && plat.x > -100) {
|
if (plat.x < GAME_WIDTH + 100 && plat.x > -100) {
|
||||||
if (player.x + 30 > plat.x && player.x < plat.x + plat.w) {
|
if (player.x + 30 > plat.x && player.x < plat.x + plat.w) {
|
||||||
// "Passed Check": Vorher drüber, jetzt drauf/drunter
|
|
||||||
const feetOld = oldY + originalHeight;
|
const feetOld = oldY + originalHeight;
|
||||||
const feetNew = newY + originalHeight;
|
const feetNew = newY + originalHeight;
|
||||||
if (feetOld <= plat.y && feetNew >= plat.y) {
|
if (feetOld <= plat.y && feetNew >= plat.y) {
|
||||||
@@ -55,7 +43,7 @@ function updateGameLogic() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- BODEN ---
|
|
||||||
if (!landed && newY + originalHeight >= GROUND_Y) {
|
if (!landed && newY + originalHeight >= GROUND_Y) {
|
||||||
newY = GROUND_Y - originalHeight;
|
newY = GROUND_Y - originalHeight;
|
||||||
player.vy = 0;
|
player.vy = 0;
|
||||||
@@ -69,34 +57,28 @@ function updateGameLogic() {
|
|||||||
player.y = newY;
|
player.y = newY;
|
||||||
player.grounded = landed;
|
player.grounded = landed;
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// 3. PUFFER BEWEGEN (STREAMING)
|
|
||||||
// ===============================================
|
|
||||||
|
|
||||||
obstacleBuffer.forEach(o => o.x -= currentSpeed);
|
obstacleBuffer.forEach(o => o.x -= currentSpeed);
|
||||||
platformBuffer.forEach(p => p.x -= currentSpeed);
|
platformBuffer.forEach(p => p.x -= currentSpeed);
|
||||||
|
|
||||||
// Aufräumen (Links raus)
|
|
||||||
obstacleBuffer = obstacleBuffer.filter(o => o.x + (o.w||30) > -200); // Muss -200 sein
|
obstacleBuffer = obstacleBuffer.filter(o => o.x + (o.w||30) > -200); // Muss -200 sein
|
||||||
platformBuffer = platformBuffer.filter(p => p.x + (p.w||100) > -200); // Muss -200 sein
|
platformBuffer = platformBuffer.filter(p => p.x + (p.w||100) > -200); // Muss -200 sein
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// 4. KOLLISION & TRANSFER (LOGIK + RENDER LISTE)
|
|
||||||
// ===============================================
|
|
||||||
|
|
||||||
obstacles = [];
|
obstacles = [];
|
||||||
platforms = [];
|
platforms = [];
|
||||||
const RENDER_LIMIT = 900;
|
const RENDER_LIMIT = 900;
|
||||||
|
|
||||||
// Hitbox definieren (für lokale Prüfung)
|
|
||||||
const pHitbox = { x: player.x, y: drawY, w: player.w, h: player.h };
|
const pHitbox = { x: player.x, y: drawY, w: player.w, h: player.h };
|
||||||
|
|
||||||
// --- HINDERNISSE ---
|
|
||||||
obstacleBuffer.forEach(obs => {
|
obstacleBuffer.forEach(obs => {
|
||||||
// Nur verarbeiten, wenn im Sichtbereich
|
|
||||||
if (obs.x < RENDER_LIMIT) {
|
if (obs.x < RENDER_LIMIT) {
|
||||||
|
|
||||||
// A. Metadaten laden (falls noch nicht da)
|
|
||||||
if (!obs.def) {
|
if (!obs.def) {
|
||||||
let baseDef = null;
|
let baseDef = null;
|
||||||
if(gameConfig && gameConfig.obstacles) {
|
if(gameConfig && gameConfig.obstacles) {
|
||||||
@@ -115,8 +97,6 @@ function updateGameLogic() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// B. Kollision prüfen (Nur wenn noch nicht eingesammelt)
|
|
||||||
// Wir nutzen 'obs.collected' als Flag, damit wir Coins nicht doppelt zählen
|
|
||||||
if (!obs.collected && !isGameOver) {
|
if (!obs.collected && !isGameOver) {
|
||||||
if (checkCollision(pHitbox, obs)) {
|
if (checkCollision(pHitbox, obs)) {
|
||||||
|
|
||||||
@@ -125,8 +105,8 @@ function updateGameLogic() {
|
|||||||
|
|
||||||
// 1. COIN
|
// 1. COIN
|
||||||
if (type === "coin") {
|
if (type === "coin") {
|
||||||
score += 2000; // Sofort addieren!
|
score += 2000;
|
||||||
obs.collected = true; // Markieren als "weg"
|
obs.collected = true;
|
||||||
playSound('coin');
|
playSound('coin');
|
||||||
spawnParticles(obs.x + 15, obs.y + 15, 'sparkle', 10);
|
spawnParticles(obs.x + 15, obs.y + 15, 'sparkle', 10);
|
||||||
}
|
}
|
||||||
@@ -134,27 +114,27 @@ function updateGameLogic() {
|
|||||||
else if (type === "powerup") {
|
else if (type === "powerup") {
|
||||||
if (id === "p_god") godModeLives = 3;
|
if (id === "p_god") godModeLives = 3;
|
||||||
if (id === "p_bat") hasBat = true;
|
if (id === "p_bat") hasBat = true;
|
||||||
if (id === "p_boot") bootTicks = 600; // ca. 10 Sekunden
|
if (id === "p_boot") bootTicks = 600;
|
||||||
playSound('powerup');
|
playSound('powerup');
|
||||||
spawnParticles(obs.x + 15, obs.y + 15, 'sparkle', 20); // Mehr Partikel
|
spawnParticles(obs.x + 15, obs.y + 15, 'sparkle', 20); // Mehr Partikel
|
||||||
|
|
||||||
obs.collected = true; // Markieren als "weg"
|
obs.collected = true;
|
||||||
}
|
}
|
||||||
// 3. GEGNER (Teacher/Obstacle)
|
|
||||||
else {
|
else {
|
||||||
// Baseballschläger vs Lehrer
|
|
||||||
if (hasBat && type === "teacher") {
|
if (hasBat && type === "teacher") {
|
||||||
hasBat = false;
|
hasBat = false;
|
||||||
obs.collected = true; // Wegschlagen
|
obs.collected = true;
|
||||||
playSound('hit');
|
playSound('hit');
|
||||||
spawnParticles(obs.x, obs.y, 'explosion', 5);
|
spawnParticles(obs.x, obs.y, 'explosion', 5);
|
||||||
// Effekt?
|
|
||||||
}
|
}
|
||||||
// Godmode (Schild)
|
// Godmode (Schild)
|
||||||
else if (godModeLives > 0) {
|
else if (godModeLives > 0) {
|
||||||
godModeLives--;
|
godModeLives--;
|
||||||
// Optional: Gegner entfernen oder durchlaufen lassen?
|
playSound('hit');
|
||||||
// Hier entfernen wir ihn, damit man nicht 2 Leben im selben Objekt verliert
|
spawnParticles(obs.x, obs.y, 'explosion', 5);
|
||||||
obs.collected = true;
|
obs.collected = true;
|
||||||
}
|
}
|
||||||
// TOT
|
// TOT
|
||||||
@@ -170,14 +150,13 @@ function updateGameLogic() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// C. Zur Render-Liste hinzufügen (Nur wenn NICHT eingesammelt)
|
|
||||||
if (!obs.collected) {
|
if (!obs.collected) {
|
||||||
obstacles.push(obs);
|
obstacles.push(obs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- PLATTFORMEN ---
|
|
||||||
platformBuffer.forEach(plat => {
|
platformBuffer.forEach(plat => {
|
||||||
if (plat.x < RENDER_LIMIT) {
|
if (plat.x < RENDER_LIMIT) {
|
||||||
platforms.push(plat);
|
platforms.push(plat);
|
||||||
@@ -185,17 +164,17 @@ function updateGameLogic() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Robuste Kollisionsprüfung
|
|
||||||
function checkCollision(p, obs) {
|
function checkCollision(p, obs) {
|
||||||
const def = obs.def || {};
|
const def = obs.def || {};
|
||||||
const w = def.width || obs.w || 30;
|
const w = def.width || obs.w || 30;
|
||||||
const h = def.height || obs.h || 30;
|
const h = def.height || obs.h || 30;
|
||||||
|
|
||||||
// Kleines Padding, damit es fair ist
|
|
||||||
const padX = 8;
|
const padX = 8;
|
||||||
const padY = (def.type === "teacher" || def.type === "principal") ? 20 : 5;
|
const padY = (def.type === "teacher" || def.type === "principal") ? 20 : 5;
|
||||||
|
|
||||||
// Koordinaten
|
|
||||||
const pL = p.x + padX;
|
const pL = p.x + padX;
|
||||||
const pR = p.x + p.w - padX;
|
const pR = p.x + p.w - padX;
|
||||||
const pT = p.y + padY;
|
const pT = p.y + padY;
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ async function loadAssets() {
|
|||||||
await Promise.all([pPromise, ...bgPromises, ...obsPromises]);
|
await Promise.all([pPromise, ...bgPromises, ...obsPromises]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 2. SPIEL STARTEN
|
|
||||||
// ==========================================
|
|
||||||
window.startGameClick = async function() {
|
window.startGameClick = async function() {
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
|
|
||||||
@@ -50,9 +48,7 @@ window.startGameClick = async function() {
|
|||||||
resize();
|
resize();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 3. GAME OVER & HIGHSCORE LOGIK
|
|
||||||
// ==========================================
|
|
||||||
window.gameOver = function(reason) {
|
window.gameOver = function(reason) {
|
||||||
if (isGameOver) return;
|
if (isGameOver) return;
|
||||||
isGameOver = true;
|
isGameOver = true;
|
||||||
@@ -69,16 +65,16 @@ window.gameOver = function(reason) {
|
|||||||
gameOverScreen.style.display = 'flex';
|
gameOverScreen.style.display = 'flex';
|
||||||
document.getElementById('finalScore').innerText = finalScore;
|
document.getElementById('finalScore').innerText = finalScore;
|
||||||
|
|
||||||
// Input wieder anzeigen
|
|
||||||
document.getElementById('inputSection').style.display = 'flex';
|
document.getElementById('inputSection').style.display = 'flex';
|
||||||
document.getElementById('submitBtn').disabled = false;
|
document.getElementById('submitBtn').disabled = false;
|
||||||
|
|
||||||
// Liste laden
|
|
||||||
loadLeaderboard();
|
loadLeaderboard();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Name absenden (Button Click)
|
|
||||||
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();
|
||||||
@@ -91,14 +87,14 @@ window.submitScore = async function() {
|
|||||||
const res = await fetch('/api/submit-name', {
|
const res = await fetch('/api/submit-name', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({ sessionId: sessionID, name: name }) // sessionID aus state.js
|
body: JSON.stringify({ sessionId: sessionID, name: name })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) throw new Error("Fehler beim Senden");
|
if (!res.ok) throw new Error("Fehler beim Senden");
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
// Code lokal speichern (Claims)
|
|
||||||
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
let myClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||||
myClaims.push({
|
myClaims.push({
|
||||||
name: name,
|
name: name,
|
||||||
@@ -109,7 +105,7 @@ 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}`);
|
||||||
@@ -121,10 +117,10 @@ window.submitScore = async function() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bestenliste laden (Game Over Screen)
|
|
||||||
async function loadLeaderboard() {
|
async function loadLeaderboard() {
|
||||||
try {
|
try {
|
||||||
// sessionID wird mitgesendet, um den eigenen Eintrag zu markieren
|
|
||||||
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();
|
||||||
|
|
||||||
@@ -149,9 +145,7 @@ async function loadLeaderboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 4. GAME LOOP
|
|
||||||
// ==========================================
|
|
||||||
function gameLoop(timestamp) {
|
function gameLoop(timestamp) {
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
|
|
||||||
@@ -183,9 +177,7 @@ function gameLoop(timestamp) {
|
|||||||
drawGame(isGameRunning ? accumulator / MS_PER_TICK : 1.0);
|
drawGame(isGameRunning ? accumulator / MS_PER_TICK : 1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 5. INIT
|
|
||||||
// ==========================================
|
|
||||||
async function initGame() {
|
async function initGame() {
|
||||||
try {
|
try {
|
||||||
const cRes = await fetch('/api/config');
|
const cRes = await fetch('/api/config');
|
||||||
@@ -215,7 +207,7 @@ async function initGame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: Mini-Leaderboard auf Startseite
|
|
||||||
async function loadStartScreenLeaderboard() {
|
async function loadStartScreenLeaderboard() {
|
||||||
try {
|
try {
|
||||||
const listEl = document.getElementById('startLeaderboardList');
|
const listEl = document.getElementById('startLeaderboardList');
|
||||||
@@ -235,15 +227,13 @@ async function loadStartScreenLeaderboard() {
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio Toggle Funktion für den Button
|
|
||||||
window.toggleAudioClick = function() {
|
window.toggleAudioClick = function() {
|
||||||
// 1. Audio umschalten (in audio.js)
|
|
||||||
const muted = toggleMute();
|
const muted = toggleMute();
|
||||||
|
|
||||||
// 2. Button Icon updaten
|
|
||||||
updateMuteIcon(muted);
|
updateMuteIcon(muted);
|
||||||
|
|
||||||
// 3. Fokus vom Button nehmen (damit Space nicht den Button drückt, sondern springt)
|
|
||||||
document.getElementById('mute-btn').blur();
|
document.getElementById('mute-btn').blur();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -256,19 +246,15 @@ function updateMuteIcon(isMuted) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// MEINE CODES (LOCAL STORAGE)
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
// 1. Codes anzeigen (Wird vom Button im Startscreen aufgerufen)
|
|
||||||
window.showMyCodes = function() {
|
window.showMyCodes = function() {
|
||||||
// Modal öffnen
|
|
||||||
openModal('codes');
|
openModal('codes');
|
||||||
|
|
||||||
const listEl = document.getElementById('codesList');
|
const listEl = document.getElementById('codesList');
|
||||||
if(!listEl) return;
|
if(!listEl) return;
|
||||||
|
|
||||||
// Daten aus dem Browser-Speicher holen
|
|
||||||
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
const rawClaims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||||
|
|
||||||
if (rawClaims.length === 0) {
|
if (rawClaims.length === 0) {
|
||||||
@@ -276,7 +262,7 @@ window.showMyCodes = function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sortieren nach Score (Höchster zuerst)
|
|
||||||
const sortedClaims = rawClaims
|
const sortedClaims = rawClaims
|
||||||
.map((item, index) => ({ ...item, originalIndex: index }))
|
.map((item, index) => ({ ...item, originalIndex: index }))
|
||||||
.sort((a, b) => b.score - a.score);
|
.sort((a, b) => b.score - a.score);
|
||||||
@@ -284,7 +270,7 @@ window.showMyCodes = function() {
|
|||||||
let html = "";
|
let html = "";
|
||||||
|
|
||||||
sortedClaims.forEach(c => {
|
sortedClaims.forEach(c => {
|
||||||
// Icons basierend auf Score
|
|
||||||
let rankIcon = "📄";
|
let rankIcon = "📄";
|
||||||
if (c.score >= 5000) rankIcon = "⭐";
|
if (c.score >= 5000) rankIcon = "⭐";
|
||||||
if (c.score >= 10000) rankIcon = "🔥";
|
if (c.score >= 10000) rankIcon = "🔥";
|
||||||
@@ -307,11 +293,11 @@ window.showMyCodes = function() {
|
|||||||
listEl.innerHTML = html;
|
listEl.innerHTML = html;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 2. Code löschen (Lokal und auf Server)
|
|
||||||
window.deleteClaim = async function(sid, code) {
|
window.deleteClaim = async function(sid, code) {
|
||||||
if(!confirm("Eintrag wirklich löschen?")) return;
|
if(!confirm("Eintrag wirklich löschen?")) return;
|
||||||
|
|
||||||
// Versuch, es auf dem Server zu löschen
|
|
||||||
try {
|
try {
|
||||||
await fetch('/api/claim/delete', {
|
await fetch('/api/claim/delete', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -322,25 +308,23 @@ window.deleteClaim = async function(sid, code) {
|
|||||||
console.warn("Server Delete fehlgeschlagen (vielleicht schon weg), lösche lokal...");
|
console.warn("Server Delete fehlgeschlagen (vielleicht schon weg), lösche lokal...");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lokal löschen
|
|
||||||
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
let claims = JSON.parse(localStorage.getItem('escape_claims') || '[]');
|
||||||
// Wir filtern den Eintrag raus, der die gleiche SessionID UND den gleichen Code hat
|
|
||||||
claims = claims.filter(c => c.code !== code);
|
claims = claims.filter(c => c.code !== code);
|
||||||
|
|
||||||
localStorage.setItem('escape_claims', JSON.stringify(claims));
|
localStorage.setItem('escape_claims', JSON.stringify(claims));
|
||||||
|
|
||||||
// Liste aktualisieren
|
|
||||||
window.showMyCodes();
|
window.showMyCodes();
|
||||||
|
|
||||||
// Leaderboard aktualisieren (falls im Hintergrund sichtbar)
|
|
||||||
if(document.getElementById('startLeaderboardList')) {
|
if(document.getElementById('startLeaderboardList')) {
|
||||||
loadStartScreenLeaderboard();
|
loadStartScreenLeaderboard();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// MODAL LOGIK (Fenster auf/zu)
|
|
||||||
// ==========================================
|
|
||||||
window.openModal = function(id) {
|
window.openModal = function(id) {
|
||||||
const el = document.getElementById('modal-' + id);
|
const el = document.getElementById('modal-' + id);
|
||||||
if(el) el.style.display = 'flex';
|
if(el) el.style.display = 'flex';
|
||||||
@@ -351,7 +335,7 @@ window.closeModal = function() {
|
|||||||
modals.forEach(el => el.style.display = 'none');
|
modals.forEach(el => el.style.display = 'none');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Klick nebendran schließt Modal
|
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
if (event.target.classList.contains('modal-overlay')) {
|
if (event.target.classList.contains('modal-overlay')) {
|
||||||
closeModal();
|
closeModal();
|
||||||
|
|||||||
@@ -1,35 +1,18 @@
|
|||||||
// ==========================================
|
|
||||||
// NETZWERK LOGIK (WEBSOCKET + RTT SYNC)
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
/*
|
|
||||||
GLOBALE VARIABLEN (aus state.js):
|
|
||||||
- socket
|
|
||||||
- obstacleBuffer, platformBuffer
|
|
||||||
- currentLatencyMs, pingInterval
|
|
||||||
- isGameRunning, isGameOver
|
|
||||||
- score, currentTick
|
|
||||||
*/
|
|
||||||
|
|
||||||
function connectGame() {
|
function connectGame() {
|
||||||
// Alte Verbindung schließen
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
socket.close();
|
socket.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping Timer stoppen falls aktiv
|
|
||||||
if (typeof pingInterval !== 'undefined' && pingInterval) {
|
if (typeof pingInterval !== 'undefined' && pingInterval) {
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protokoll automatisch wählen (ws:// oder wss://)
|
|
||||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const url = proto + "//" + location.host + "/ws";
|
const url = proto + "//" + location.host + "/ws";
|
||||||
|
|
||||||
console.log("Verbinde zu:", url);
|
console.log("Verbinde zu:", url);
|
||||||
socket = new WebSocket(url);
|
socket = new WebSocket(url);
|
||||||
|
|
||||||
// --- 1. VERBINDUNG GEÖFFNET ---
|
|
||||||
socket.onopen = () => {
|
socket.onopen = () => {
|
||||||
console.log("🟢 WS Verbunden. Spiel startet.");
|
console.log("🟢 WS Verbunden. Spiel startet.");
|
||||||
|
|
||||||
@@ -38,88 +21,72 @@ function connectGame() {
|
|||||||
platformBuffer = [];
|
platformBuffer = [];
|
||||||
obstacles = [];
|
obstacles = [];
|
||||||
platforms = [];
|
platforms = [];
|
||||||
currentLatencyMs = 0; // Reset Latenz
|
currentLatencyMs = 0;
|
||||||
|
|
||||||
isGameRunning = true;
|
isGameRunning = true;
|
||||||
isGameOver = false;
|
isGameOver = false;
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
|
|
||||||
// PING LOOP STARTEN (Jede Sekunde messen)
|
|
||||||
pingInterval = setInterval(sendPing, 1000);
|
pingInterval = setInterval(sendPing, 1000);
|
||||||
|
|
||||||
// Game Loop anwerfen
|
|
||||||
requestAnimationFrame(gameLoop);
|
requestAnimationFrame(gameLoop);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 2. NACHRICHT VOM SERVER ---
|
|
||||||
socket.onmessage = (event) => {
|
socket.onmessage = (event) => {
|
||||||
try {
|
try {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
|
|
||||||
// A. PONG (Latenzmessung)
|
|
||||||
if (msg.type === "pong") {
|
if (msg.type === "pong") {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const sentTime = msg.ts; // Server schickt unseren Timestamp zurück
|
const sentTime = msg.ts;
|
||||||
|
|
||||||
|
|
||||||
// Round Trip Time (Hin + Zurück)
|
|
||||||
const rtt = now - sentTime;
|
const rtt = now - sentTime;
|
||||||
|
|
||||||
// One Way Latency (Latenz in eine Richtung)
|
|
||||||
const latency = rtt / 2;
|
const latency = rtt / 2;
|
||||||
|
|
||||||
// Glätten (Exponential Moving Average), damit Werte nicht springen
|
|
||||||
// Wenn es der erste Wert ist, nehmen wir ihn direkt.
|
|
||||||
if (currentLatencyMs === 0) {
|
if (currentLatencyMs === 0) {
|
||||||
currentLatencyMs = latency;
|
currentLatencyMs = latency;
|
||||||
} else {
|
} else {
|
||||||
// 90% alter Wert, 10% neuer Wert
|
|
||||||
currentLatencyMs = (currentLatencyMs * 0.9) + (latency * 0.1);
|
currentLatencyMs = (currentLatencyMs * 0.9) + (latency * 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: Debugging im Log
|
|
||||||
// console.log(`📡 Ping: ${rtt}ms | Latenz: ${currentLatencyMs.toFixed(1)}ms`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// B. CHUNK (Objekte empfangen)
|
|
||||||
if (msg.type === "chunk") {
|
if (msg.type === "chunk") {
|
||||||
|
|
||||||
// 1. CLOCK SYNC (Die Zeitmaschine)
|
|
||||||
// Wenn der Server bei Tick 204 ist und wir bei 182, müssen wir aufholen!
|
|
||||||
// Wir addieren die geschätzte Latenz (in Ticks) auf die Serverzeit.
|
|
||||||
// 60 FPS = 16ms/Tick. 20 TPS = 50ms/Tick.
|
|
||||||
|
|
||||||
const msPerTick = 1000 / 20; // WICHTIG: Wir laufen auf 20 TPS Basis!
|
const msPerTick = 1000 / 20;
|
||||||
const latencyInTicks = Math.floor(currentLatencyMs / msPerTick);
|
const latencyInTicks = Math.floor(currentLatencyMs / msPerTick);
|
||||||
|
|
||||||
// Ziel-Zeit: Server-Zeit + Übertragungsweg
|
|
||||||
const targetTick = msg.serverTick + latencyInTicks;
|
const targetTick = msg.serverTick + latencyInTicks;
|
||||||
const drift = targetTick - currentTick;
|
const drift = targetTick - currentTick;
|
||||||
|
|
||||||
// Wenn wir mehr als 2 Ticks abweichen -> Korrigieren
|
|
||||||
if (Math.abs(drift) > 2) {
|
if (Math.abs(drift) > 2) {
|
||||||
// console.log(`⏰ Clock Sync: ${currentTick} -> ${targetTick} (Drift: ${drift})`);
|
currentTick = targetTick;
|
||||||
currentTick = targetTick; // Harter Sync, damit Physik stimmt
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. PIXEL KORREKTUR (Sanfter!)
|
|
||||||
// Wir berechnen den Speed
|
|
||||||
let sTick = msg.serverTick;
|
let sTick = msg.serverTick;
|
||||||
// Formel aus logic.js (Base 15 + Zeit)
|
|
||||||
let currentSpeedPerTick = 15.0 + (sTick / 1000.0) * 1.5;
|
let currentSpeedPerTick = 15.0 + (sTick / 1000.0) * 1.5;
|
||||||
if (currentSpeedPerTick > 36) currentSpeedPerTick = 36;
|
if (currentSpeedPerTick > 36) currentSpeedPerTick = 36;
|
||||||
|
|
||||||
const speedPerMs = currentSpeedPerTick / msPerTick; // Speed pro MS
|
const speedPerMs = currentSpeedPerTick / msPerTick;
|
||||||
|
|
||||||
|
|
||||||
// Korrektur: Latenz * Speed
|
|
||||||
// FIX: Wir kappen die Korrektur bei max 100px, damit Objekte nicht "teleportieren".
|
|
||||||
let dynamicCorrection = (currentLatencyMs * speedPerMs) + 5;
|
let dynamicCorrection = (currentLatencyMs * speedPerMs) + 5;
|
||||||
if (dynamicCorrection > 100) dynamicCorrection = 100; // Limit
|
if (dynamicCorrection > 100) dynamicCorrection = 100; // Limit
|
||||||
|
|
||||||
// Puffer füllen (mit Limit)
|
|
||||||
if (msg.obstacles) {
|
if (msg.obstacles) {
|
||||||
msg.obstacles.forEach(o => {
|
msg.obstacles.forEach(o => {
|
||||||
o.x -= dynamicCorrection;
|
o.x -= dynamicCorrection;
|
||||||
// Init für Interpolation
|
|
||||||
o.prevX = o.x;
|
o.prevX = o.x;
|
||||||
obstacleBuffer.push(o);
|
obstacleBuffer.push(o);
|
||||||
});
|
});
|
||||||
@@ -135,7 +102,6 @@ function connectGame() {
|
|||||||
|
|
||||||
if (msg.score !== undefined) score = msg.score;
|
if (msg.score !== undefined) score = msg.score;
|
||||||
|
|
||||||
// Powerups übernehmen (für Anzeige)
|
|
||||||
if (msg.powerups) {
|
if (msg.powerups) {
|
||||||
godModeLives = msg.powerups.godLives;
|
godModeLives = msg.powerups.godLives;
|
||||||
hasBat = msg.powerups.hasBat;
|
hasBat = msg.powerups.hasBat;
|
||||||
@@ -154,7 +120,6 @@ function connectGame() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// C. TOD (Server Authoritative)
|
|
||||||
if (msg.type === "dead") {
|
if (msg.type === "dead") {
|
||||||
console.log("💀 Server sagt: Game Over");
|
console.log("💀 Server sagt: Game Over");
|
||||||
|
|
||||||
@@ -171,15 +136,11 @@ function connectGame() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 1. CLIENT SPEED BERECHNEN (Formel aus logic.js)
|
|
||||||
// Wir nutzen hier 'score', da logic.js das auch tut
|
|
||||||
let clientSpeed = BASE_SPEED + (currentTick / 1000.0) * 1.5;
|
let clientSpeed = BASE_SPEED + (currentTick / 1000.0) * 1.5;
|
||||||
if (clientSpeed > 36.0) clientSpeed = 36.0;
|
if (clientSpeed > 36.0) clientSpeed = 36.0;
|
||||||
|
|
||||||
// 2. SERVER SPEED HOLEN
|
|
||||||
let serverSpeed = msg.currentSpeed || 0;
|
let serverSpeed = msg.currentSpeed || 0;
|
||||||
|
|
||||||
// 3. DIFF BERECHNEN
|
|
||||||
let diffSpeed = clientSpeed - serverSpeed;
|
let diffSpeed = clientSpeed - serverSpeed;
|
||||||
let speedIcon = Math.abs(diffSpeed) < 0.01 ? "✅" : "❌";
|
let speedIcon = Math.abs(diffSpeed) < 0.01 ? "✅" : "❌";
|
||||||
|
|
||||||
@@ -194,12 +155,10 @@ function connectGame() {
|
|||||||
console.warn(`⚠️ ACHTUNG: Geschwindigkeiten weichen ab! Diff: ${diffSpeed.toFixed(4)}`);
|
console.warn(`⚠️ ACHTUNG: Geschwindigkeiten weichen ab! Diff: ${diffSpeed.toFixed(4)}`);
|
||||||
console.warn("Ursache: Client nutzt 'Score', Server nutzt 'Ticks'. Sind diese synchron?");
|
console.warn("Ursache: Client nutzt 'Score', Server nutzt 'Ticks'. Sind diese synchron?");
|
||||||
}
|
}
|
||||||
// -----------------------------
|
|
||||||
|
|
||||||
// 1. Hindernisse vergleichen
|
|
||||||
generateSyncTable("Obstacles", obstacles, msg.obstacles);
|
generateSyncTable("Obstacles", obstacles, msg.obstacles);
|
||||||
|
|
||||||
// 2. Plattformen vergleichen
|
|
||||||
generateSyncTable("Platforms", platforms, msg.platforms);
|
generateSyncTable("Platforms", platforms, msg.platforms);
|
||||||
|
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
@@ -210,7 +169,7 @@ function connectGame() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 3. VERBINDUNG GETRENNT ---
|
|
||||||
socket.onclose = () => {
|
socket.onclose = () => {
|
||||||
console.log("🔴 WS Verbindung getrennt.");
|
console.log("🔴 WS Verbindung getrennt.");
|
||||||
if (pingInterval) clearInterval(pingInterval);
|
if (pingInterval) clearInterval(pingInterval);
|
||||||
@@ -221,13 +180,9 @@ function connectGame() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// PING SENDEN
|
|
||||||
// ==========================================
|
|
||||||
function sendPing() {
|
function sendPing() {
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
// Wir senden den aktuellen Zeitstempel
|
|
||||||
// Der Server muss diesen im "tick" Feld zurückschicken (siehe websocket.go)
|
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
type: "ping",
|
type: "ping",
|
||||||
tick: Date.now() // Timestamp als Integer
|
tick: Date.now() // Timestamp als Integer
|
||||||
@@ -235,9 +190,6 @@ function sendPing() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// INPUT SENDEN
|
|
||||||
// ==========================================
|
|
||||||
function sendInput(type, action) {
|
function sendInput(type, action) {
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
socket.send(JSON.stringify({
|
socket.send(JSON.stringify({
|
||||||
@@ -256,37 +208,33 @@ function generateSyncTable(label, clientList, serverList) {
|
|||||||
const report = [];
|
const report = [];
|
||||||
const matchedServerIndices = new Set();
|
const matchedServerIndices = new Set();
|
||||||
|
|
||||||
// 1. Parameter für Latenz-Korrektur berechnen
|
|
||||||
// Damit wir wissen: "Wo MÜSSTE das Server-Objekt auf dem Client sein?"
|
|
||||||
const msPerTick = 50; // Bei 20 TPS
|
|
||||||
|
|
||||||
// Speed Schätzung (gleiche Formel wie in logic.js)
|
const msPerTick = 50;
|
||||||
|
|
||||||
|
|
||||||
let debugSpeed = 15.0 + (score / 1000.0) * 1.5;
|
let debugSpeed = 15.0 + (score / 1000.0) * 1.5;
|
||||||
if (debugSpeed > 36) debugSpeed = 36;
|
if (debugSpeed > 36) debugSpeed = 36;
|
||||||
|
|
||||||
const speedPerMs = debugSpeed / msPerTick;
|
const speedPerMs = debugSpeed / msPerTick;
|
||||||
|
|
||||||
// Pixel, die das Objekt wegen Ping weiter "links" sein müsste
|
|
||||||
const latencyPx = currentLatencyMs * speedPerMs;
|
const latencyPx = currentLatencyMs * speedPerMs;
|
||||||
|
|
||||||
// 2. Client Objekte durchgehen
|
|
||||||
clientList.forEach((cObj) => {
|
clientList.forEach((cObj) => {
|
||||||
let bestMatch = null;
|
let bestMatch = null;
|
||||||
let bestDist = 9999;
|
let bestDist = 9999;
|
||||||
let bestSIdx = -1;
|
let bestSIdx = -1;
|
||||||
|
|
||||||
// ID sicherstellen
|
|
||||||
const cID = cObj.def ? cObj.def.id : (cObj.id || "unknown");
|
const cID = cObj.def ? cObj.def.id : (cObj.id || "unknown");
|
||||||
|
|
||||||
// Passendes Server-Objekt suchen
|
|
||||||
serverList.forEach((sObj, sIdx) => {
|
serverList.forEach((sObj, sIdx) => {
|
||||||
if (matchedServerIndices.has(sIdx)) return;
|
if (matchedServerIndices.has(sIdx)) return;
|
||||||
|
|
||||||
const sID = sObj.id || "unknown";
|
const sID = sObj.id || "unknown";
|
||||||
|
|
||||||
// Match Kriterien:
|
|
||||||
// 1. Gleiche ID (oder Plattform)
|
|
||||||
// 2. Nähe (Wir vergleichen hier die korrigierte Position!)
|
|
||||||
const sPosCorrected = sObj.x - latencyPx;
|
const sPosCorrected = sObj.x - latencyPx;
|
||||||
const dist = Math.abs(cObj.x - sPosCorrected);
|
const dist = Math.abs(cObj.x - sPosCorrected);
|
||||||
|
|
||||||
@@ -310,12 +258,10 @@ function generateSyncTable(label, clientList, serverList) {
|
|||||||
matchedServerIndices.add(bestSIdx);
|
matchedServerIndices.add(bestSIdx);
|
||||||
|
|
||||||
serverXRaw = bestMatch.x;
|
serverXRaw = bestMatch.x;
|
||||||
serverXCorrected = bestMatch.x - latencyPx; // Hier rechnen wir den Ping raus
|
serverXCorrected = bestMatch.x - latencyPx;
|
||||||
|
|
||||||
// Der "Wahrs" Drift: Differenz nach Latenz-Abzug
|
|
||||||
diffReal = cObj.x - serverXCorrected;
|
diffReal = cObj.x - serverXCorrected;
|
||||||
|
|
||||||
// Status Bestimmung
|
|
||||||
const absDiff = Math.abs(diffReal);
|
const absDiff = Math.abs(diffReal);
|
||||||
if (absDiff < 20) status = "✅ PERFECT";
|
if (absDiff < 20) status = "✅ PERFECT";
|
||||||
else if (absDiff < 60) status = "🆗 OK";
|
else if (absDiff < 60) status = "🆗 OK";
|
||||||
@@ -333,10 +279,8 @@ function generateSyncTable(label, clientList, serverList) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Fehlende Server Objekte finden
|
|
||||||
serverList.forEach((sObj, sIdx) => {
|
serverList.forEach((sObj, sIdx) => {
|
||||||
if (!matchedServerIndices.has(sIdx)) {
|
if (!matchedServerIndices.has(sIdx)) {
|
||||||
// Prüfen, ob es vielleicht einfach noch unsichtbar ist (Zukunft)
|
|
||||||
const sPosCorrected = sObj.x - latencyPx;
|
const sPosCorrected = sObj.x - latencyPx;
|
||||||
let status = "❌ MISSING";
|
let status = "❌ MISSING";
|
||||||
|
|
||||||
@@ -354,7 +298,6 @@ function generateSyncTable(label, clientList, serverList) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. Sortieren nach Position (links nach rechts)
|
|
||||||
report.sort((a, b) => {
|
report.sort((a, b) => {
|
||||||
const valA = (typeof a["Client X"] === 'number') ? a["Client X"] : a["Server X (Sim)"];
|
const valA = (typeof a["Client X"] === 'number') ? a["Client X"] : a["Server X (Sim)"];
|
||||||
const valB = (typeof b["Client X"] === 'number') ? b["Client X"] : b["Server X (Sim)"];
|
const valB = (typeof b["Client X"] === 'number') ? b["Client X"] : b["Server X (Sim)"];
|
||||||
|
|||||||
@@ -5,23 +5,20 @@ class Particle {
|
|||||||
constructor(x, y, type) {
|
constructor(x, y, type) {
|
||||||
this.x = x;
|
this.x = x;
|
||||||
this.y = y;
|
this.y = y;
|
||||||
this.life = 1.0; // 1.0 = 100% Leben
|
this.life = 1.0;
|
||||||
this.type = type; // 'dust', 'sparkle', 'explosion'
|
this.type = type;
|
||||||
|
|
||||||
// Zufällige Geschwindigkeit
|
|
||||||
const angle = Math.random() * Math.PI * 2;
|
const angle = Math.random() * Math.PI * 2;
|
||||||
let speed = Math.random() * 2;
|
let speed = Math.random() * 2;
|
||||||
|
|
||||||
if (type === 'dust') {
|
if (type === 'dust') {
|
||||||
// Staub fliegt eher nach oben/hinten
|
|
||||||
this.vx = -2 + Math.random();
|
this.vx = -2 + Math.random();
|
||||||
this.vy = -1 - Math.random();
|
this.vy = -1 - Math.random();
|
||||||
this.decay = 0.05; // Verschwindet schnell
|
this.decay = 0.05;
|
||||||
this.color = '#ddd';
|
this.color = '#ddd';
|
||||||
this.size = Math.random() * 4 + 2;
|
this.size = Math.random() * 4 + 2;
|
||||||
}
|
}
|
||||||
else if (type === 'sparkle') {
|
else if (type === 'sparkle') {
|
||||||
// Münzen glitzern in alle Richtungen
|
|
||||||
speed = Math.random() * 4 + 2;
|
speed = Math.random() * 4 + 2;
|
||||||
this.vx = Math.cos(angle) * speed;
|
this.vx = Math.cos(angle) * speed;
|
||||||
this.vy = Math.sin(angle) * speed;
|
this.vy = Math.sin(angle) * speed;
|
||||||
@@ -30,7 +27,6 @@ class Particle {
|
|||||||
this.size = Math.random() * 3 + 1;
|
this.size = Math.random() * 3 + 1;
|
||||||
}
|
}
|
||||||
else if (type === 'explosion') {
|
else if (type === 'explosion') {
|
||||||
// Tod
|
|
||||||
speed = Math.random() * 6 + 2;
|
speed = Math.random() * 6 + 2;
|
||||||
this.vx = Math.cos(angle) * speed;
|
this.vx = Math.cos(angle) * speed;
|
||||||
this.vy = Math.sin(angle) * speed;
|
this.vy = Math.sin(angle) * speed;
|
||||||
@@ -65,7 +61,6 @@ class Particle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- API ---
|
|
||||||
|
|
||||||
function spawnParticles(x, y, type, count = 5) {
|
function spawnParticles(x, y, type, count = 5) {
|
||||||
for(let i=0; i<count; i++) {
|
for(let i=0; i<count; i++) {
|
||||||
@@ -74,7 +69,6 @@ function spawnParticles(x, y, type, count = 5) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateParticles() {
|
function updateParticles() {
|
||||||
// Rückwärts loopen zum sicheren Löschen
|
|
||||||
for (let i = particles.length - 1; i >= 0; i--) {
|
for (let i = particles.length - 1; i >= 0; i--) {
|
||||||
particles[i].update();
|
particles[i].update();
|
||||||
if (particles[i].life <= 0) {
|
if (particles[i].life <= 0) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// RESIZE LOGIK (LETTERBOXING)
|
// RESIZE LOGIK (LETTERBOXING)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
function resize() {
|
function resize() {
|
||||||
// 1. Interne Auflösung fixieren
|
|
||||||
canvas.width = GAME_WIDTH; // 800
|
canvas.width = GAME_WIDTH; // 800
|
||||||
canvas.height = GAME_HEIGHT; // 400
|
canvas.height = GAME_HEIGHT; // 400
|
||||||
|
|
||||||
@@ -35,20 +34,13 @@ window.addEventListener('resize', resize);
|
|||||||
resize();
|
resize();
|
||||||
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// DRAWING LOOP (MIT INTERPOLATION)
|
|
||||||
// ==========================================
|
|
||||||
// alpha (0.0 bis 1.0) gibt an, wie weit wir zeitlich zwischen zwei Physik-Ticks sind.
|
|
||||||
function drawGame(alpha = 1.0) {
|
function drawGame(alpha = 1.0) {
|
||||||
// 1. Canvas leeren
|
|
||||||
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
ctx.clearRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// HINTERGRUND
|
|
||||||
// ===============================================
|
|
||||||
let currentBg = null;
|
let currentBg = null;
|
||||||
if (bgSprites.length > 0) {
|
if (bgSprites.length > 0) {
|
||||||
// Wechselt alle 10.000 Punkte
|
|
||||||
const changeInterval = 10000;
|
const changeInterval = 10000;
|
||||||
const currentRawIndex = Math.floor(score / changeInterval);
|
const currentRawIndex = Math.floor(score / changeInterval);
|
||||||
if (currentRawIndex > maxRawBgIndex) maxRawBgIndex = currentRawIndex;
|
if (currentRawIndex > maxRawBgIndex) maxRawBgIndex = currentRawIndex;
|
||||||
@@ -63,63 +55,49 @@ function drawGame(alpha = 1.0) {
|
|||||||
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
ctx.fillRect(0, 0, GAME_WIDTH, GAME_HEIGHT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// BODEN
|
|
||||||
// ===============================================
|
|
||||||
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
|
ctx.fillStyle = "rgba(60, 60, 60, 0.8)";
|
||||||
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
|
ctx.fillRect(0, GROUND_Y, GAME_WIDTH, 50);
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// PLATTFORMEN (Interpoliert)
|
|
||||||
// ===============================================
|
|
||||||
platforms.forEach(p => {
|
platforms.forEach(p => {
|
||||||
// Interpolierte X-Position
|
|
||||||
const rX = (p.prevX !== undefined) ? lerp(p.prevX, p.x, alpha) : p.x;
|
const rX = (p.prevX !== undefined) ? lerp(p.prevX, p.x, alpha) : p.x;
|
||||||
const rY = p.y;
|
const rY = p.y;
|
||||||
|
|
||||||
// Holz-Optik
|
|
||||||
ctx.fillStyle = "#5D4037";
|
ctx.fillStyle = "#5D4037";
|
||||||
ctx.fillRect(rX, rY, p.w, p.h);
|
ctx.fillRect(rX, rY, p.w, p.h);
|
||||||
ctx.fillStyle = "#8D6E63";
|
ctx.fillStyle = "#8D6E63";
|
||||||
ctx.fillRect(rX, rY, p.w, 5); // Highlight oben
|
ctx.fillRect(rX, rY, p.w, 5);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// HINDERNISSE (Interpoliert)
|
|
||||||
// ===============================================
|
|
||||||
obstacles.forEach(obs => {
|
obstacles.forEach(obs => {
|
||||||
const def = obs.def || {};
|
const def = obs.def || {};
|
||||||
const img = sprites[def.id];
|
const img = sprites[def.id];
|
||||||
|
|
||||||
// Interpolation
|
|
||||||
const rX = (obs.prevX !== undefined) ? lerp(obs.prevX, obs.x, alpha) : obs.x;
|
const rX = (obs.prevX !== undefined) ? lerp(obs.prevX, obs.x, alpha) : obs.x;
|
||||||
const rY = obs.y;
|
const rY = obs.y;
|
||||||
|
|
||||||
// Hitbox Dimensionen
|
|
||||||
const hbw = def.width || obs.w || 30;
|
const hbw = def.width || obs.w || 30;
|
||||||
const hbh = def.height || obs.h || 30;
|
const hbh = def.height || obs.h || 30;
|
||||||
|
|
||||||
if (img && img.complete && img.naturalHeight !== 0) {
|
if (img && img.complete && img.naturalHeight !== 0) {
|
||||||
// --- BILD VORHANDEN ---
|
|
||||||
// Editor-Werte anwenden
|
|
||||||
const scale = def.imgScale || 1.0;
|
const scale = def.imgScale || 1.0;
|
||||||
const offX = def.imgOffsetX || 0.0;
|
const offX = def.imgOffsetX || 0.0;
|
||||||
const offY = def.imgOffsetY || 0.0;
|
const offY = def.imgOffsetY || 0.0;
|
||||||
|
|
||||||
// 1. Skalierte Größe
|
|
||||||
const drawW = hbw * scale;
|
const drawW = hbw * scale;
|
||||||
const drawH = hbh * scale;
|
const drawH = hbh * scale;
|
||||||
|
|
||||||
// 2. Positionierung (Zentriert & Unten Bündig zur Hitbox)
|
|
||||||
const baseX = rX + (hbw - drawW) / 2;
|
const baseX = rX + (hbw - drawW) / 2;
|
||||||
const baseY = rY + (hbh - drawH);
|
const baseY = rY + (hbh - drawH);
|
||||||
|
|
||||||
// 3. Zeichnen
|
|
||||||
ctx.drawImage(img, baseX + offX, baseY + offY, drawW, drawH);
|
ctx.drawImage(img, baseX + offX, baseY + offY, drawW, drawH);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// --- FALLBACK (KEIN BILD) ---
|
|
||||||
// Magenta als Warnung, Gold für Coins
|
|
||||||
let color = "#FF00FF";
|
let color = "#FF00FF";
|
||||||
if (def.type === "coin") color = "gold";
|
if (def.type === "coin") color = "gold";
|
||||||
else if (def.color) color = def.color;
|
else if (def.color) color = def.color;
|
||||||
@@ -127,50 +105,36 @@ function drawGame(alpha = 1.0) {
|
|||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.fillRect(rX, rY, hbw, hbh);
|
ctx.fillRect(rX, rY, hbw, hbh);
|
||||||
|
|
||||||
// Rahmen & Text
|
|
||||||
ctx.strokeStyle = "rgba(255,255,255,0.5)"; ctx.lineWidth = 2;
|
ctx.strokeStyle = "rgba(255,255,255,0.5)"; ctx.lineWidth = 2;
|
||||||
ctx.strokeRect(rX, rY, hbw, hbh);
|
ctx.strokeRect(rX, rY, hbw, hbh);
|
||||||
ctx.fillStyle = "white"; ctx.font = "bold 10px monospace";
|
ctx.fillStyle = "white"; ctx.font = "bold 10px monospace";
|
||||||
ctx.fillText(def.id || "?", rX, rY - 5);
|
ctx.fillText(def.id || "?", rX, rY - 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- DEBUG HITBOX (Client) ---
|
|
||||||
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
||||||
ctx.strokeStyle = "rgba(0,255,0,0.5)"; // Grün transparent
|
ctx.strokeStyle = "rgba(0,255,0,0.5)"; // Grün transparent
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
ctx.strokeRect(rX, rY, hbw, hbh);
|
ctx.strokeRect(rX, rY, hbw, hbh);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sprechblase
|
|
||||||
if(obs.speech) drawSpeechBubble(rX, rY, obs.speech);
|
if(obs.speech) drawSpeechBubble(rX, rY, obs.speech);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// DEBUG: SERVER STATE (Cyan)
|
|
||||||
// ===============================================
|
|
||||||
// Zeigt an, wo der Server die Objekte sieht (ohne Interpolation)
|
|
||||||
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
if (typeof DEBUG_SYNC !== 'undefined' && DEBUG_SYNC) {
|
||||||
if (serverObstacles) {
|
if (serverObstacles) {
|
||||||
ctx.strokeStyle = "cyan";
|
ctx.strokeStyle = "cyan";
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
serverObstacles.forEach(sObj => {
|
serverObstacles.forEach(sObj => {
|
||||||
// Wir müssen hier die Latenz-Korrektur aus network.js abziehen,
|
|
||||||
// um zu sehen, wo network.js sie hingeschoben hat?
|
|
||||||
// Nein, serverObstacles enthält die Rohdaten.
|
|
||||||
// Wenn wir wissen wollen, wo der Server "jetzt" ist, müssten wir schätzen.
|
|
||||||
// Wir zeichnen einfach Raw, das hinkt optisch meist hinterher.
|
|
||||||
ctx.strokeRect(sObj.x, sObj.y, sObj.w, sObj.h);
|
ctx.strokeRect(sObj.x, sObj.y, sObj.w, sObj.h);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// SPIELER (Interpoliert)
|
|
||||||
// ===============================================
|
|
||||||
// Interpolierte Y-Position
|
|
||||||
let rPlayerY = lerp(player.prevY !== undefined ? player.prevY : player.y, player.y, alpha);
|
let rPlayerY = lerp(player.prevY !== undefined ? player.prevY : player.y, player.y, alpha);
|
||||||
|
|
||||||
// Ducken Anpassung
|
|
||||||
const drawY = isCrouching ? rPlayerY + 25 : rPlayerY;
|
const drawY = isCrouching ? rPlayerY + 25 : rPlayerY;
|
||||||
const drawH = isCrouching ? 25 : 50;
|
const drawH = isCrouching ? 25 : 50;
|
||||||
|
|
||||||
@@ -181,16 +145,11 @@ function drawGame(alpha = 1.0) {
|
|||||||
ctx.fillRect(player.x, drawY, player.w, drawH);
|
ctx.fillRect(player.x, drawY, player.w, drawH);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// PARTIKEL (Visuelle Effekte)
|
|
||||||
// ===============================================
|
|
||||||
if (typeof drawParticles === 'function') {
|
if (typeof drawParticles === 'function') {
|
||||||
drawParticles();
|
drawParticles();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// HUD (Statusanzeige)
|
|
||||||
// ===============================================
|
|
||||||
if (isGameRunning && !isGameOver) {
|
if (isGameRunning && !isGameOver) {
|
||||||
ctx.fillStyle = "black";
|
ctx.fillStyle = "black";
|
||||||
ctx.font = "bold 10px monospace";
|
ctx.font = "bold 10px monospace";
|
||||||
@@ -206,9 +165,6 @@ function drawGame(alpha = 1.0) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===============================================
|
|
||||||
// GAME OVER OVERLAY
|
|
||||||
// ===============================================
|
|
||||||
if (isGameOver) {
|
if (isGameOver) {
|
||||||
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
ctx.fillStyle = "rgba(0,0,0,0.7)";
|
||||||
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
|
ctx.fillRect(0,0,GAME_WIDTH, GAME_HEIGHT);
|
||||||
|
|||||||
@@ -1,41 +1,35 @@
|
|||||||
// ==========================================
|
|
||||||
// GLOBALE STATUS VARIABLEN
|
|
||||||
// ==========================================
|
|
||||||
|
|
||||||
// --- Konfiguration & Flags ---
|
let gameConfig = null;
|
||||||
let gameConfig = null; // Wird von /api/config geladen
|
let isLoaded = false;
|
||||||
let isLoaded = false; // Sind Assets geladen?
|
let isGameRunning = false;
|
||||||
let isGameRunning = false; // Läuft der Game Loop?
|
let isGameOver = false;
|
||||||
let isGameOver = false; // Ist der Spieler tot?
|
let sessionID = null;
|
||||||
let sessionID = null; // UUID der aktuellen Session
|
|
||||||
|
|
||||||
// --- NETZWERK & STREAMING (NEU) ---
|
let socket = null;
|
||||||
let socket = null; // Die WebSocket Verbindung
|
let obstacleBuffer = [];
|
||||||
let obstacleBuffer = []; // Warteschlange für kommende Hindernisse
|
let platformBuffer = [];
|
||||||
let platformBuffer = []; // Warteschlange für kommende Plattformen
|
|
||||||
|
let score = 0;
|
||||||
|
let currentTick = 0;
|
||||||
|
|
||||||
// --- SPIELZUSTAND ---
|
|
||||||
let score = 0; // Aktueller Punktestand (vom Server diktiert)
|
|
||||||
let currentTick = 0; // Zeit-Einheit des Spiels
|
|
||||||
|
|
||||||
// --- POWERUPS (Client Visuals) ---
|
|
||||||
let godModeLives = 0;
|
let godModeLives = 0;
|
||||||
let hasBat = false;
|
let hasBat = false;
|
||||||
let bootTicks = 0;
|
let bootTicks = 0;
|
||||||
|
|
||||||
// --- HINTERGRUND ---
|
|
||||||
let maxRawBgIndex = 0; // Welcher Hintergrund wird gezeigt?
|
|
||||||
|
|
||||||
// --- GAME LOOP TIMING ---
|
let maxRawBgIndex = 0;
|
||||||
|
|
||||||
|
|
||||||
let lastTime = 0;
|
let lastTime = 0;
|
||||||
let accumulator = 0;
|
let accumulator = 0;
|
||||||
|
|
||||||
// --- GRAFIKEN ---
|
|
||||||
let sprites = {}; // Cache für Hindernis-Bilder
|
|
||||||
let playerSprite = new Image();
|
|
||||||
let bgSprites = []; // Array der Hintergrund-Bilder
|
|
||||||
|
|
||||||
// --- ENTITIES (Render-Listen) ---
|
let sprites = {};
|
||||||
|
let playerSprite = new Image();
|
||||||
|
let bgSprites = [];
|
||||||
|
|
||||||
|
|
||||||
let player = {
|
let player = {
|
||||||
x: 50,
|
x: 50,
|
||||||
y: 300,
|
y: 300,
|
||||||
@@ -49,28 +43,25 @@ let player = {
|
|||||||
let particles = [];
|
let particles = [];
|
||||||
|
|
||||||
|
|
||||||
// Diese Listen werden von logic.js aus dem Buffer gefüllt und von render.js gezeichnet
|
|
||||||
let obstacles = [];
|
let obstacles = [];
|
||||||
let platforms = [];
|
let platforms = [];
|
||||||
|
|
||||||
// Debug-Daten (optional, falls der Server Debug-Infos schickt)
|
|
||||||
let serverObstacles = [];
|
let serverObstacles = [];
|
||||||
let serverPlatforms = [];
|
let serverPlatforms = [];
|
||||||
|
|
||||||
let currentLatencyMs = 0; // Aktuelle Latenz in Millisekunden
|
let currentLatencyMs = 0;
|
||||||
let pingInterval = null; // Timer für den Ping
|
let pingInterval = null;
|
||||||
|
|
||||||
|
|
||||||
// --- INPUT STATE ---
|
|
||||||
let isCrouching = false;
|
let isCrouching = false;
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// HTML ELEMENTE (Caching)
|
|
||||||
// ==========================================
|
|
||||||
const canvas = document.getElementById('gameCanvas');
|
const canvas = document.getElementById('gameCanvas');
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const container = document.getElementById('game-container');
|
const container = document.getElementById('game-container');
|
||||||
|
|
||||||
// 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');
|
||||||
|
|||||||
@@ -12,9 +12,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* =========================================
|
|
||||||
1. GRUNDLAGEN & GLOBAL
|
|
||||||
========================================= */
|
|
||||||
body, html {
|
body, html {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
56
websocket.go
56
websocket.go
@@ -12,8 +12,8 @@ import (
|
|||||||
const (
|
const (
|
||||||
ServerTickRate = 50 * time.Millisecond
|
ServerTickRate = 50 * time.Millisecond
|
||||||
|
|
||||||
BufferAhead = 60 // Puffergröße (Zukunft)
|
BufferAhead = 60
|
||||||
SpawnXStart = 2000.0 // Spawn Abstand
|
SpawnXStart = 2000.0
|
||||||
)
|
)
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
@@ -24,7 +24,7 @@ var upgrader = websocket.Upgrader{
|
|||||||
type WSInputMsg struct {
|
type WSInputMsg struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Input string `json:"input"`
|
Input string `json:"input"`
|
||||||
Tick int `json:"tick"` // Optional: Client Timestamp für Ping
|
Tick int `json:"tick"`
|
||||||
PosY float64 `json:"y"`
|
PosY float64 `json:"y"`
|
||||||
VelY float64 `json:"vy"`
|
VelY float64 `json:"vy"`
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,7 @@ type WSServerMsg struct {
|
|||||||
Score int `json:"score"`
|
Score int `json:"score"`
|
||||||
PowerUps PowerUpState `json:"powerups"`
|
PowerUps PowerUpState `json:"powerups"`
|
||||||
SessionID string `json:"sessionId"`
|
SessionID string `json:"sessionId"`
|
||||||
Ts int `json:"ts,omitempty"` // Für Pong
|
Ts int `json:"ts,omitempty"`
|
||||||
CurrentSpeed float64 `json:"currentSpeed"`
|
CurrentSpeed float64 `json:"currentSpeed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,15 +107,7 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
case <-closeChan:
|
case <-closeChan:
|
||||||
return // Client weg
|
return // Client weg
|
||||||
|
|
||||||
// WICHTIG: Wir verarbeiten Inputs hier NICHT einzeln,
|
|
||||||
// sondern sammeln sie im Default-Case (siehe unten),
|
|
||||||
// oder nutzen eine nicht-blockierende Schleife.
|
|
||||||
// Aber für einfache Logik reicht select.
|
|
||||||
// Um "Input Lag" zu verhindern, lesen wir den Channel leer:
|
|
||||||
|
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// A. INPUTS VERARBEITEN (Alle die angekommen sind!)
|
|
||||||
// Wir loopen solange durch den Channel, bis er leer ist.
|
|
||||||
InputLoop:
|
InputLoop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -136,7 +128,6 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
if msg.Type == "ping" {
|
if msg.Type == "ping" {
|
||||||
// Sofort Pong zurück (Performance wichtig!)
|
|
||||||
conn.WriteJSON(WSServerMsg{Type: "pong", Ts: msg.Tick})
|
conn.WriteJSON(WSServerMsg{Type: "pong", Ts: msg.Tick})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,25 +154,21 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Printf("🐞 Debug Snapshot an Client gesendet (Tick %d)", state.Ticks)
|
log.Printf("🐞 Debug Snapshot an Client gesendet (Tick %d)", state.Ticks)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Channel leer, weiter zur Physik
|
|
||||||
break InputLoop
|
break InputLoop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// B. LIVE SIMULATION (1 Tick)
|
|
||||||
// Jetzt simulieren wir genau EINEN Frame (16ms)
|
|
||||||
state.Ticks++
|
state.Ticks++
|
||||||
state.Score++ // Score wächst mit der Zeit
|
state.Score++
|
||||||
|
|
||||||
currentSpeed := calculateSpeed(state.Ticks)
|
currentSpeed := calculateSpeed(state.Ticks)
|
||||||
|
|
||||||
updatePhysics(&state, pendingJump, isCrouching, currentSpeed)
|
updatePhysics(&state, pendingJump, isCrouching, currentSpeed)
|
||||||
pendingJump = false // Jump Trigger reset
|
pendingJump = false
|
||||||
|
|
||||||
checkCollisions(&state, isCrouching, currentSpeed)
|
checkCollisions(&state, isCrouching, currentSpeed)
|
||||||
|
|
||||||
if state.IsDead {
|
if state.IsDead {
|
||||||
// Score Persistieren
|
|
||||||
rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
|
rdb.HSet(ctx, "session:"+sessionID, map[string]interface{}{
|
||||||
"score": state.Score,
|
"score": state.Score,
|
||||||
"is_dead": 1,
|
"is_dead": 1,
|
||||||
@@ -194,13 +181,10 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
moveWorld(&state, currentSpeed)
|
moveWorld(&state, currentSpeed)
|
||||||
|
|
||||||
// C. STREAMING (Zukunft)
|
|
||||||
// Wir generieren nur, wenn der Puffer zur Neige geht
|
|
||||||
targetTick := state.Ticks + BufferAhead
|
targetTick := state.Ticks + BufferAhead
|
||||||
var newObs []ActiveObstacle
|
var newObs []ActiveObstacle
|
||||||
var newPlats []ActivePlatform
|
var newPlats []ActivePlatform
|
||||||
|
|
||||||
// Um CPU zu sparen, generieren wir max 10 Ticks pro Frame nach
|
|
||||||
loops := 0
|
loops := 0
|
||||||
for generatedHeadTick < targetTick && loops < 10 {
|
for generatedHeadTick < targetTick && loops < 10 {
|
||||||
generatedHeadTick++
|
generatedHeadTick++
|
||||||
@@ -216,8 +200,6 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// D. SENDEN (Effizienz)
|
|
||||||
// Nur senden wenn Daten da sind ODER alle 15 Frames (Heartbeat/Score Sync)
|
|
||||||
if len(newObs) > 0 || len(newPlats) > 0 || state.Ticks%15 == 0 {
|
if len(newObs) > 0 || len(newPlats) > 0 || state.Ticks%15 == 0 {
|
||||||
msg := WSServerMsg{
|
msg := WSServerMsg{
|
||||||
Type: "chunk",
|
Type: "chunk",
|
||||||
@@ -235,7 +217,6 @@ func handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hilfsfunktion: Generiert Objekte für EINEN Tick in der Zukunft
|
|
||||||
func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstacle, []ActivePlatform) {
|
func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstacle, []ActivePlatform) {
|
||||||
var createdObs []ActiveObstacle
|
var createdObs []ActiveObstacle
|
||||||
var createdPlats []ActivePlatform
|
var createdPlats []ActivePlatform
|
||||||
@@ -249,19 +230,12 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac
|
|||||||
if tick >= s.NextSpawnTick {
|
if tick >= s.NextSpawnTick {
|
||||||
spawnX := SpawnXStart
|
spawnX := SpawnXStart
|
||||||
|
|
||||||
// --- ENTSCHEIDUNG: CHUNK vs RANDOM ---
|
|
||||||
|
|
||||||
// Wir nutzen die globalen Chunks (da Read-Only während des Spiels, ist Zugriff sicher)
|
|
||||||
chunkCount := len(defaultConfig.Chunks)
|
chunkCount := len(defaultConfig.Chunks)
|
||||||
|
|
||||||
if chunkCount > 0 && s.RNG.NextFloat() > 0.8 {
|
if chunkCount > 0 && s.RNG.NextFloat() > 0.8 {
|
||||||
// =================================================
|
|
||||||
// OPTION A: CHUNK SPAWNING
|
|
||||||
// =================================================
|
|
||||||
idx := int(s.RNG.NextRange(0, float64(chunkCount)))
|
idx := int(s.RNG.NextRange(0, float64(chunkCount)))
|
||||||
chunk := defaultConfig.Chunks[idx]
|
chunk := defaultConfig.Chunks[idx]
|
||||||
|
|
||||||
// 1. Plattformen übernehmen
|
|
||||||
for _, p := range chunk.Platforms {
|
for _, p := range chunk.Platforms {
|
||||||
createdPlats = append(createdPlats, ActivePlatform{
|
createdPlats = append(createdPlats, ActivePlatform{
|
||||||
X: spawnX + p.X,
|
X: spawnX + p.X,
|
||||||
@@ -271,21 +245,18 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Hindernisse übernehmen & Speech berechnen
|
|
||||||
for _, o := range chunk.Obstacles {
|
for _, o := range chunk.Obstacles {
|
||||||
|
|
||||||
// Speech-Logik: Wir müssen die Original-Def finden, um zu wissen, ob er sprechen kann
|
|
||||||
speech := ""
|
speech := ""
|
||||||
for _, def := range defaultConfig.Obstacles {
|
for _, def := range defaultConfig.Obstacles {
|
||||||
if def.ID == o.ID {
|
if def.ID == o.ID {
|
||||||
// Wenn gefunden, würfeln wir
|
|
||||||
if def.CanTalk && len(def.SpeechLines) > 0 {
|
if def.CanTalk && len(def.SpeechLines) > 0 {
|
||||||
if s.RNG.NextFloat() > 0.7 { // 30% Wahrscheinlichkeit
|
if s.RNG.NextFloat() > 0.7 { // 30% Wahrscheinlichkeit
|
||||||
sIdx := int(s.RNG.NextRange(0, float64(len(def.SpeechLines))))
|
sIdx := int(s.RNG.NextRange(0, float64(len(def.SpeechLines))))
|
||||||
speech = def.SpeechLines[sIdx]
|
speech = def.SpeechLines[sIdx]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break // Def gefunden, Loop abbrechen
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,11 +267,10 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac
|
|||||||
Y: o.Y,
|
Y: o.Y,
|
||||||
Width: o.Width,
|
Width: o.Width,
|
||||||
Height: o.Height,
|
Height: o.Height,
|
||||||
Speech: speech, // <--- HIER wird der Text gesetzt
|
Speech: speech,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timer setzen (Länge des Chunks)
|
|
||||||
width := float64(chunk.TotalWidth)
|
width := float64(chunk.TotalWidth)
|
||||||
if width == 0 {
|
if width == 0 {
|
||||||
width = 2000.0
|
width = 2000.0
|
||||||
@@ -308,15 +278,10 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac
|
|||||||
s.NextSpawnTick = tick + int(width/speed)
|
s.NextSpawnTick = tick + int(width/speed)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// =================================================
|
|
||||||
// OPTION B: RANDOM SPAWNING
|
|
||||||
// =================================================
|
|
||||||
|
|
||||||
// Lücke berechnen
|
|
||||||
gap := 400 + int(s.RNG.NextRange(0, 500))
|
gap := 400 + int(s.RNG.NextRange(0, 500))
|
||||||
s.NextSpawnTick = tick + int(float64(gap)/speed)
|
s.NextSpawnTick = tick + int(float64(gap)/speed)
|
||||||
|
|
||||||
// Pool bilden (Boss Phase?)
|
|
||||||
defs := defaultConfig.Obstacles
|
defs := defaultConfig.Obstacles
|
||||||
if len(defs) > 0 {
|
if len(defs) > 0 {
|
||||||
isBoss := (tick % 1500) > 1200
|
isBoss := (tick % 1500) > 1200
|
||||||
@@ -334,11 +299,9 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Objekt auswählen
|
|
||||||
def := s.RNG.PickDef(pool)
|
def := s.RNG.PickDef(pool)
|
||||||
|
|
||||||
if def != nil {
|
if def != nil {
|
||||||
// Powerup Rarity (90% Chance, dass es NICHT spawnt)
|
|
||||||
if def.Type == "powerup" && s.RNG.NextFloat() > 0.1 {
|
if def.Type == "powerup" && s.RNG.NextFloat() > 0.1 {
|
||||||
def = nil
|
def = nil
|
||||||
}
|
}
|
||||||
@@ -353,7 +316,6 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Y-Position berechnen (Boden - Höhe - Offset)
|
|
||||||
spawnY := GroundY - def.Height - def.YOffset
|
spawnY := GroundY - def.Height - def.YOffset
|
||||||
|
|
||||||
createdObs = append(createdObs, ActiveObstacle{
|
createdObs = append(createdObs, ActiveObstacle{
|
||||||
@@ -363,7 +325,7 @@ func generateFutureObjects(s *SimState, tick int, speed float64) ([]ActiveObstac
|
|||||||
Y: spawnY,
|
Y: spawnY,
|
||||||
Width: def.Width,
|
Width: def.Width,
|
||||||
Height: def.Height,
|
Height: def.Height,
|
||||||
Speech: speech, // <--- HIER wird der Text gesetzt
|
Speech: speech,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user