diff --git a/cmd/builder/main.go b/cmd/builder/main.go index 2904c14..ce07246 100644 --- a/cmd/builder/main.go +++ b/cmd/builder/main.go @@ -30,7 +30,7 @@ import ( // --- CONFIG --- const ( RawDir = "./assets_raw" - OutFile = "./cmd/client/assets/assets.json" + OutFile = "cmd/client/web/assets/assets.json" WidthList = 280 // Etwas breiter für die Bilder WidthInspect = 300 diff --git a/cmd/client/assets_native.go b/cmd/client/assets_native.go index b391586..60904cd 100644 --- a/cmd/client/assets_native.go +++ b/cmd/client/assets_native.go @@ -17,7 +17,7 @@ import ( func (g *Game) loadAssets() { // Pfad anpassen: Wir suchen im relativen Pfad - baseDir := "./cmd/client/assets" + baseDir := "./cmd/client/web/assets" manifestPath := filepath.Join(baseDir, "assets.json") data, err := ioutil.ReadFile(manifestPath) diff --git a/cmd/client/chunks_native.go b/cmd/client/chunks_native.go index 020af05..3919616 100644 --- a/cmd/client/chunks_native.go +++ b/cmd/client/chunks_native.go @@ -10,7 +10,7 @@ import ( // loadChunks lädt alle Chunks aus dem Verzeichnis (Native Desktop) func (g *Game) loadChunks() { - baseDir := "cmd/client/assets" + baseDir := "cmd/client/web/assets" chunkDir := filepath.Join(baseDir, "chunks") err := g.world.LoadChunkLibrary(chunkDir) diff --git a/cmd/client/connection_native.go b/cmd/client/connection_native.go index 7136fc5..ea3abd7 100644 --- a/cmd/client/connection_native.go +++ b/cmd/client/connection_native.go @@ -252,17 +252,32 @@ func (g *Game) submitScore() { name = g.teamName } + // Verwende Team-Name für Coop-Mode, sonst Player-Name + displayName := name + teamName := "" + if g.gameMode == "coop" { + g.stateMutex.Lock() + teamName = g.gameState.TeamName + g.stateMutex.Unlock() + + if teamName != "" { + displayName = teamName + } + } + msg := WebSocketMessage{ Type: "score_submit", Payload: game.ScoreSubmission{ + PlayerName: displayName, PlayerCode: g.playerCode, - Name: name, + Name: displayName, // Für Kompatibilität Score: score, Mode: g.gameMode, + TeamName: teamName, // Team-Name für Coop }, } g.sendWebSocketMessage(msg) g.scoreSubmitted = true - log.Printf("📊 Score submitted: %s = %d", name, score) + log.Printf("📊 Score submitted: %s = %d (TeamName: %s)", displayName, score, teamName) } diff --git a/cmd/client/connection_wasm.go b/cmd/client/connection_wasm.go index 3f02464..e3a30c3 100644 --- a/cmd/client/connection_wasm.go +++ b/cmd/client/connection_wasm.go @@ -92,6 +92,19 @@ func (g *Game) connectToServer() { // An JavaScript senden g.sendLeaderboardToJS() } + + case "score_response": + // Score Submission Response mit Proof-Code + payloadBytes, _ := json.Marshal(msg.Payload) + var resp game.ScoreSubmissionResponse + if err := json.Unmarshal(payloadBytes, &resp); err == nil { + log.Printf("🎯 Proof-Code empfangen: %s für Score: %d", resp.ProofCode, resp.Score) + + // Proof-Code an JavaScript senden + if saveFunc := js.Global().Get("saveHighscoreCode"); !saveFunc.IsUndefined() { + saveFunc.Invoke(resp.Score, resp.ProofCode, g.playerName) + } + } } return nil @@ -259,19 +272,34 @@ func (g *Game) submitScore() { name = g.teamName } + // Verwende Team-Name für Coop-Mode, sonst Player-Name + displayName := name + teamName := "" + if g.gameMode == "coop" { + g.stateMutex.Lock() + teamName = g.gameState.TeamName + g.stateMutex.Unlock() + + if teamName != "" { + displayName = teamName + } + } + msg := WebSocketMessage{ Type: "score_submit", Payload: game.ScoreSubmission{ + PlayerName: displayName, PlayerCode: g.playerCode, - Name: name, + Name: displayName, // Für Kompatibilität Score: score, Mode: g.gameMode, + TeamName: teamName, // Team-Name für Coop }, } g.sendWebSocketMessage(msg) g.scoreSubmitted = true - log.Printf("📊 Score submitted: %s = %d", name, score) + log.Printf("📊 Score submitted: %s = %d (TeamName: %s)", displayName, score, teamName) } // Dummy-Funktionen für Kompatibilität mit anderen Teilen des Codes @@ -280,9 +308,41 @@ func (g *Game) sendJoinRequest() { } func (g *Game) sendStartRequest() { + // Warte bis WebSocket verbunden ist + for i := 0; i < 30; i++ { + if g.connected && g.wsConn != nil && g.wsConn.connected { + break + } + time.Sleep(100 * time.Millisecond) + } + + if !g.connected { + log.Println("❌ Kann START nicht senden - keine Verbindung") + return + } + g.startGame() } func (g *Game) publishInput(input game.ClientInput) { g.sendInput(input) } + +// sendSetTeamNameInput sendet SET_TEAM_NAME Input über WebSocket +func (g *Game) sendSetTeamNameInput(teamName string) { + if !g.connected { + log.Println("❌ Kann Team-Name nicht senden - keine Verbindung") + return + } + + myID := g.getMyPlayerID() + input := game.ClientInput{ + Type: "SET_TEAM_NAME", + RoomID: g.roomID, + PlayerID: myID, + TeamName: teamName, + } + + g.sendInput(input) + log.Printf("🏷️ SET_TEAM_NAME gesendet: '%s'", teamName) +} diff --git a/cmd/client/game_render.go b/cmd/client/game_render.go index a4cf967..c45abd9 100644 --- a/cmd/client/game_render.go +++ b/cmd/client/game_render.go @@ -5,10 +5,7 @@ import ( "image/color" "log" "math" - "sort" - "time" - "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/text" @@ -222,12 +219,11 @@ func (g *Game) DrawGame(screen *ebiten.Image) { // In WASM: HTML Game Over Screen anzeigen if !g.scoreSubmitted { - g.scoreSubmitted = true - g.submitScore() + g.submitScore() // submitScore() setzt g.scoreSubmitted intern g.sendGameOverToJS(myScore) // Zeigt HTML Game Over Screen } - g.DrawGameOverLeaderboard(screen, myScore) + g.drawGameOverScreen(screen, myScore) return // Früher Return, damit Game-UI nicht mehr gezeichnet wird } @@ -510,153 +506,3 @@ func (g *Game) DrawAsset(screen *ebiten.Image, assetID string, worldX, worldY fl } } -// DrawGameOverLeaderboard zeigt Leaderboard mit Team-Name-Eingabe -func (g *Game) DrawGameOverLeaderboard(screen *ebiten.Image, myScore int) { - screen.Fill(color.RGBA{20, 20, 30, 255}) - - // Leaderboard immer beim ersten Mal anfordern (ohne Lock hier!) - if !g.scoreSubmitted && g.gameMode == "solo" { - g.submitScore() // submitScore() ruft requestLeaderboard() auf - } else { - // Für Coop: Nur Leaderboard anfordern, nicht submitten - g.leaderboardMutex.Lock() - needsLeaderboard := len(g.leaderboard) == 0 && g.connected - g.leaderboardMutex.Unlock() - - if needsLeaderboard { - g.requestLeaderboard() - } - } - - // Großes GAME OVER - text.Draw(screen, "GAME OVER", basicfont.Face7x13, ScreenWidth/2-50, 60, color.RGBA{255, 0, 0, 255}) - - // Linke Seite: Raum-Ergebnisse - Daten KOPIEREN mit Lock, dann außerhalb zeichnen - text.Draw(screen, "=== RAUM ERGEBNISSE ===", basicfont.Face7x13, 50, 120, color.RGBA{255, 255, 0, 255}) - - type playerScore struct { - name string - score int - } - - // Lock NUR für Datenkopie - g.stateMutex.Lock() - players := make([]playerScore, 0, len(g.gameState.Players)) - for _, p := range g.gameState.Players { - players = append(players, playerScore{name: p.Name, score: p.Score}) - } - g.stateMutex.Unlock() - - // Sortieren und Zeichnen OHNE Lock - sort.Slice(players, func(i, j int) bool { - return players[i].score > players[j].score - }) - - y := 150 - for i, p := range players { - medal := "" - if i == 0 { - medal = "🥇 " - } else if i == 1 { - medal = "🥈 " - } else if i == 2 { - medal = "🥉 " - } - scoreMsg := fmt.Sprintf("%d. %s%s: %d pts", i+1, medal, p.name, p.score) - text.Draw(screen, scoreMsg, basicfont.Face7x13, 50, y, color.White) - y += 20 - } - - // Rechte Seite: Global Leaderboard - Daten KOPIEREN mit Lock, dann außerhalb zeichnen - text.Draw(screen, "=== TOP 10 BESTENLISTE ===", basicfont.Face7x13, 650, 120, color.RGBA{255, 215, 0, 255}) - - // Lock NUR für Datenkopie - g.leaderboardMutex.Lock() - leaderboardCopy := make([]game.LeaderboardEntry, len(g.leaderboard)) - copy(leaderboardCopy, g.leaderboard) - g.leaderboardMutex.Unlock() - - // Zeichnen OHNE Lock - ly := 150 - if len(leaderboardCopy) == 0 { - text.Draw(screen, "Laden...", basicfont.Face7x13, 700, ly, color.Gray{150}) - } else { - for i, entry := range leaderboardCopy { - if i >= 10 { - break - } - var col color.Color = color.White - marker := "" - if entry.PlayerCode == g.playerCode { - col = color.RGBA{0, 255, 0, 255} - marker = " ← DU" - } - medal := "" - if i == 0 { - medal = "🥇 " - } else if i == 1 { - medal = "🥈 " - } else if i == 2 { - medal = "🥉 " - } - leaderMsg := fmt.Sprintf("%d. %s%s: %d%s", i+1, medal, entry.PlayerName, entry.Score, marker) - text.Draw(screen, leaderMsg, basicfont.Face7x13, 650, ly, col) - ly += 20 - } - } - - // Team-Name-Eingabe nur für Coop-Host (in der Mitte unten) - if g.gameMode == "coop" && g.isHost { - text.Draw(screen, "Host: Gib Team-Namen ein", basicfont.Face7x13, ScreenWidth/2-100, ScreenHeight-180, color.RGBA{255, 215, 0, 255}) - - // Team-Name Feld - fieldW := 300 - fieldX := ScreenWidth/2 - fieldW/2 - fieldY := ScreenHeight - 140 - - col := color.RGBA{70, 70, 80, 255} - if g.activeField == "teamname" { - col = color.RGBA{90, 90, 100, 255} - } - vector.DrawFilledRect(screen, float32(fieldX), float32(fieldY), float32(fieldW), 40, col, false) - vector.StrokeRect(screen, float32(fieldX), float32(fieldY), float32(fieldW), 40, 2, color.RGBA{255, 215, 0, 255}, false) - - display := g.teamName - if g.activeField == "teamname" && (time.Now().UnixMilli()/500)%2 == 0 { - display += "|" - } - if display == "" { - display = "Team Name..." - } - text.Draw(screen, display, basicfont.Face7x13, fieldX+10, fieldY+25, color.White) - - // Submit Button - submitBtnY := ScreenHeight - 85 - submitBtnW := 200 - submitBtnX := ScreenWidth/2 - submitBtnW/2 - - btnCol := color.RGBA{0, 150, 0, 255} - if g.teamName == "" { - btnCol = color.RGBA{100, 100, 100, 255} // Grau wenn kein Name - } - vector.DrawFilledRect(screen, float32(submitBtnX), float32(submitBtnY), float32(submitBtnW), 40, btnCol, false) - vector.StrokeRect(screen, float32(submitBtnX), float32(submitBtnY), float32(submitBtnW), 40, 2, color.White, false) - text.Draw(screen, "SUBMIT SCORE", basicfont.Face7x13, submitBtnX+50, submitBtnY+25, color.White) - } else if g.gameMode == "solo" && g.scoreSubmitted { - // Solo: Zeige Bestätigungsmeldung - text.Draw(screen, "Score eingereicht!", basicfont.Face7x13, ScreenWidth/2-70, ScreenHeight-100, color.RGBA{0, 255, 0, 255}) - } else if g.gameMode == "coop" && !g.isHost { - // Coop Non-Host: Warten auf Host - text.Draw(screen, "Warte auf Host...", basicfont.Face7x13, ScreenWidth/2-70, ScreenHeight-100, color.Gray{180}) - } - - // Back Button (oben links) - backBtnW, backBtnH := 120, 40 - backBtnX, backBtnY := 20, 20 - vector.DrawFilledRect(screen, float32(backBtnX), float32(backBtnY), float32(backBtnW), float32(backBtnH), color.RGBA{150, 0, 0, 255}, false) - vector.StrokeRect(screen, float32(backBtnX), float32(backBtnY), float32(backBtnW), float32(backBtnH), 2, color.White, false) - text.Draw(screen, "< ZURÜCK", basicfont.Face7x13, backBtnX+20, backBtnY+25, color.White) - - // Unten: Anleitung - text.Draw(screen, "ESC oder ZURÜCK-Button = Menü", basicfont.Face7x13, ScreenWidth/2-110, ScreenHeight-30, color.Gray{180}) -} diff --git a/cmd/client/gameover_native.go b/cmd/client/gameover_native.go index 23ab410..b27df6a 100644 --- a/cmd/client/gameover_native.go +++ b/cmd/client/gameover_native.go @@ -3,7 +3,172 @@ package main +import ( + "fmt" + "image/color" + "sort" + "time" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text" + "github.com/hajimehoshi/ebiten/v2/vector" + "golang.org/x/image/font/basicfont" + + "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" +) + // sendGameOverToJS ist ein Stub für Native (kein HTML) func (g *Game) sendGameOverToJS(score int) { // Native hat kein HTML-Overlay, nichts zu tun } + +// drawGameOverScreen zeigt Leaderboard mit Team-Name-Eingabe +func (g *Game) drawGameOverScreen(screen *ebiten.Image, myScore int) { + screen.Fill(color.RGBA{20, 20, 30, 255}) + + // Leaderboard immer beim ersten Mal anfordern (ohne Lock hier!) + if !g.scoreSubmitted && g.gameMode == "solo" { + g.submitScore() // submitScore() ruft requestLeaderboard() auf + } else { + // Für Coop: Nur Leaderboard anfordern, nicht submitten + g.leaderboardMutex.Lock() + needsLeaderboard := len(g.leaderboard) == 0 && g.connected + g.leaderboardMutex.Unlock() + + if needsLeaderboard { + g.requestLeaderboard() + } + } + + // Großes GAME OVER + text.Draw(screen, "GAME OVER", basicfont.Face7x13, ScreenWidth/2-50, 60, color.RGBA{255, 0, 0, 255}) + + // Linke Seite: Raum-Ergebnisse - Daten KOPIEREN mit Lock, dann außerhalb zeichnen + text.Draw(screen, "=== RAUM ERGEBNISSE ===", basicfont.Face7x13, 50, 120, color.RGBA{255, 255, 0, 255}) + + type playerScore struct { + name string + score int + } + + // Lock NUR für Datenkopie + g.stateMutex.Lock() + players := make([]playerScore, 0, len(g.gameState.Players)) + for _, p := range g.gameState.Players { + players = append(players, playerScore{name: p.Name, score: p.Score}) + } + g.stateMutex.Unlock() + + // Sortieren und Zeichnen OHNE Lock + sort.Slice(players, func(i, j int) bool { + return players[i].score > players[j].score + }) + + y := 150 + for i, p := range players { + medal := "" + if i == 0 { + medal = "🥇 " + } else if i == 1 { + medal = "🥈 " + } else if i == 2 { + medal = "🥉 " + } + scoreMsg := fmt.Sprintf("%d. %s%s: %d pts", i+1, medal, p.name, p.score) + text.Draw(screen, scoreMsg, basicfont.Face7x13, 50, y, color.White) + y += 20 + } + + // Rechte Seite: Global Leaderboard - Daten KOPIEREN mit Lock, dann außerhalb zeichnen + text.Draw(screen, "=== TOP 10 BESTENLISTE ===", basicfont.Face7x13, 650, 120, color.RGBA{255, 215, 0, 255}) + + // Lock NUR für Datenkopie + g.leaderboardMutex.Lock() + leaderboardCopy := make([]game.LeaderboardEntry, len(g.leaderboard)) + copy(leaderboardCopy, g.leaderboard) + g.leaderboardMutex.Unlock() + + // Zeichnen OHNE Lock + ly := 150 + if len(leaderboardCopy) == 0 { + text.Draw(screen, "Laden...", basicfont.Face7x13, 700, ly, color.Gray{150}) + } else { + for i, entry := range leaderboardCopy { + if i >= 10 { + break + } + var col color.Color = color.White + marker := "" + if entry.PlayerCode == g.playerCode { + col = color.RGBA{0, 255, 0, 255} + marker = " ← DU" + } + medal := "" + if i == 0 { + medal = "🥇 " + } else if i == 1 { + medal = "🥈 " + } else if i == 2 { + medal = "🥉 " + } + leaderMsg := fmt.Sprintf("%d. %s%s: %d%s", i+1, medal, entry.PlayerName, entry.Score, marker) + text.Draw(screen, leaderMsg, basicfont.Face7x13, 650, ly, col) + ly += 20 + } + } + + // Team-Name-Eingabe nur für Coop-Host (in der Mitte unten) + if g.gameMode == "coop" && g.isHost { + text.Draw(screen, "Host: Gib Team-Namen ein", basicfont.Face7x13, ScreenWidth/2-100, ScreenHeight-180, color.RGBA{255, 215, 0, 255}) + + // Team-Name Feld + fieldW := 300 + fieldX := ScreenWidth/2 - fieldW/2 + fieldY := ScreenHeight - 140 + + col := color.RGBA{70, 70, 80, 255} + if g.activeField == "teamname" { + col = color.RGBA{90, 90, 100, 255} + } + vector.DrawFilledRect(screen, float32(fieldX), float32(fieldY), float32(fieldW), 40, col, false) + vector.StrokeRect(screen, float32(fieldX), float32(fieldY), float32(fieldW), 40, 2, color.RGBA{255, 215, 0, 255}, false) + + display := g.teamName + if g.activeField == "teamname" && (time.Now().UnixMilli()/500)%2 == 0 { + display += "|" + } + if display == "" { + display = "Team Name..." + } + text.Draw(screen, display, basicfont.Face7x13, fieldX+10, fieldY+25, color.White) + + // Submit Button + submitBtnY := ScreenHeight - 85 + submitBtnW := 200 + submitBtnX := ScreenWidth/2 - submitBtnW/2 + + btnCol := color.RGBA{0, 150, 0, 255} + if g.teamName == "" { + btnCol = color.RGBA{100, 100, 100, 255} // Grau wenn kein Name + } + vector.DrawFilledRect(screen, float32(submitBtnX), float32(submitBtnY), float32(submitBtnW), 40, btnCol, false) + vector.StrokeRect(screen, float32(submitBtnX), float32(submitBtnY), float32(submitBtnW), 40, 2, color.White, false) + text.Draw(screen, "SUBMIT SCORE", basicfont.Face7x13, submitBtnX+50, submitBtnY+25, color.White) + } else if g.gameMode == "solo" && g.scoreSubmitted { + // Solo: Zeige Bestätigungsmeldung + text.Draw(screen, "Score eingereicht!", basicfont.Face7x13, ScreenWidth/2-70, ScreenHeight-100, color.RGBA{0, 255, 0, 255}) + } else if g.gameMode == "coop" && !g.isHost { + // Coop Non-Host: Warten auf Host + text.Draw(screen, "Warte auf Host...", basicfont.Face7x13, ScreenWidth/2-70, ScreenHeight-100, color.Gray{180}) + } + + // Back Button (oben links) + backBtnW, backBtnH := 120, 40 + backBtnX, backBtnY := 20, 20 + vector.DrawFilledRect(screen, float32(backBtnX), float32(backBtnY), float32(backBtnW), float32(backBtnH), color.RGBA{150, 0, 0, 255}, false) + vector.StrokeRect(screen, float32(backBtnX), float32(backBtnY), float32(backBtnW), float32(backBtnH), 2, color.White, false) + text.Draw(screen, "< ZURÜCK", basicfont.Face7x13, backBtnX+20, backBtnY+25, color.White) + + // Unten: Anleitung + text.Draw(screen, "ESC oder ZURÜCK-Button = Menü", basicfont.Face7x13, ScreenWidth/2-110, ScreenHeight-30, color.Gray{180}) +} diff --git a/cmd/client/gameover_wasm.go b/cmd/client/gameover_wasm.go new file mode 100644 index 0000000..f2305bd --- /dev/null +++ b/cmd/client/gameover_wasm.go @@ -0,0 +1,16 @@ +//go:build js && wasm +// +build js,wasm + +package main + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2" +) + +// drawGameOverScreen zeichnet NICHTS in WASM - HTML übernimmt +func (g *Game) drawGameOverScreen(screen *ebiten.Image, myScore int) { + // In WASM: HTML Game Over Screen ist aktiv, zeichne nur schwarzen Hintergrund + screen.Fill(color.RGBA{0, 0, 0, 255}) +} diff --git a/cmd/client/wasm_bridge.go b/cmd/client/wasm_bridge.go index 7187edb..0a8d78d 100644 --- a/cmd/client/wasm_bridge.go +++ b/cmd/client/wasm_bridge.go @@ -28,11 +28,11 @@ func (g *Game) setupJavaScriptBridge() { g.savePlayerName(playerName) if mode == "solo" { - // Solo Mode - direkt ins Spiel + // Solo Mode - Auto-Start wartet auf Server g.roomID = fmt.Sprintf("solo_%d", time.Now().UnixNano()) g.isHost = true - g.appState = StateGame - log.Printf("🎮 Solo-Spiel gestartet: %s", playerName) + g.appState = StateLobby // Warte auf Server Auto-Start + log.Printf("🎮 Solo-Spiel gestartet: %s (warte auf Server)", playerName) } else if mode == "coop" && len(args) >= 5 { // Co-op Mode - in die Lobby roomID := args[2].String() @@ -87,12 +87,24 @@ func (g *Game) setupJavaScriptBridge() { return nil }) + // setTeamName_WASM(teamName) - Host setzt Team-Namen + setTeamNameFunc := js.FuncOf(func(this js.Value, args []js.Value) interface{} { + if len(args) > 0 { + teamName := args[0].String() + g.teamName = teamName + log.Printf("🏷️ Team-Name gesetzt: '%s'", teamName) + go g.sendSetTeamNameInput(teamName) + } + return nil + }) + // Im globalen Scope registrieren js.Global().Set("startGame", startGameFunc) js.Global().Set("requestLeaderboard", requestLeaderboardFunc) js.Global().Set("setMusicVolume", setMusicVolumeFunc) js.Global().Set("setSFXVolume", setSFXVolumeFunc) js.Global().Set("startGameFromLobby_WASM", startGameFromLobbyFunc) + js.Global().Set("setTeamName_WASM", setTeamNameFunc) log.Println("✅ JavaScript Bridge registriert") } @@ -131,6 +143,7 @@ func (g *Game) sendLobbyPlayersToJS() { g.stateMutex.Lock() players := make([]interface{}, 0, len(g.gameState.Players)) hostID := g.gameState.HostID + teamName := g.gameState.TeamName for id, p := range g.gameState.Players { name := p.Name @@ -151,4 +164,12 @@ func (g *Game) sendLobbyPlayersToJS() { updateFunc.Invoke(jsPlayers) log.Printf("👥 Lobby-Spieler an JavaScript gesendet: %d Spieler", len(players)) } + + // Team-Name an JavaScript senden + if updateTeamFunc := js.Global().Get("updateLobbyTeamName"); !updateTeamFunc.IsUndefined() { + myID := g.getMyPlayerID() + isHost := (myID == hostID) + updateTeamFunc.Invoke(teamName, isHost) + log.Printf("🏷️ Team-Name an JavaScript gesendet: '%s' (isHost: %v)", teamName, isHost) + } } diff --git a/cmd/client/web/README.md b/cmd/client/web/README.md deleted file mode 100644 index 12ccf76..0000000 --- a/cmd/client/web/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Escape From Teacher - WASM Web Version - -## 🎮 Starten - -```bash -# Im web-Verzeichnis einen HTTP-Server starten -cd cmd/client/web -python3 -m http.server 8000 -``` - -Dann im Browser öffnen: **http://localhost:8000** - -## ✨ Features - -### Modernes HTML/CSS Menü -- **Custom Font**: Nutzt `front.ttf` aus dem assets-Ordner -- **Responsive Design**: Funktioniert auf Desktop, Tablet und Smartphone -- **Glassmorphism**: Moderner durchsichtiger Look mit Blur-Effekt -- **Smooth Animations**: Fade-in, Slide-up, Pulse-Effekte - -### Menü-Optionen -1. **Solo spielen**: Einzelspieler-Modus -2. **Co-op spielen**: Multiplayer mit Raum-Code -3. **Leaderboard**: Top 10 Spieler anzeigen -4. **Einstellungen**: Musik & SFX Lautstärke einstellen - -### Mobile-Optimiert -- Touch-freundliche Buttons -- Responsive Layout für kleine Bildschirme -- Viewport-Anpassung für Smartphones - -## 🎨 Design-Features - -- **Gradient Background**: Lila-Blauer Farbverlauf -- **Button Hover-Effekte**: Ripple-Animationen -- **Custom Scrollbars**: Für Leaderboard -- **Loading Screen**: Spinner während WASM lädt -- **Emoji-Icons**: 🏃 👥 🏆 ⚙️ - -## 📁 Dateien - -- `index.html`: HTML-Struktur -- `style.css`: Alle Styles mit Custom Font -- `game.js`: JavaScript-Bridge zwischen HTML und WASM -- `wasm_exec.js`: Go WASM Runtime (von Go kopiert) -- `main.wasm`: Kompiliertes Spiel - -## 🔧 Entwicklung - -### WASM neu kompilieren -```bash -GOOS=js GOARCH=wasm go build -o cmd/client/web/main.wasm ./cmd/client -``` - -### Font ändern -Ersetze `cmd/client/assets/front.ttf` und aktualisiere den Pfad in `style.css`: -```css -@font-face { - font-family: 'GameFont'; - src: url('../assets/front.ttf') format('truetype'); -} -``` - -## 🎯 Keyboard Shortcuts - -- **ESC**: Zurück zum Menü (während des Spiels) - -## 📱 Mobile Testing - -Für lokales Mobile Testing: -```bash -python3 -m http.server 8000 -# Öffne auf Smartphone: http://[DEINE-IP]:8000 -``` - -## 🚀 Production Deployment - -Für Production alle Dateien hochladen: -- index.html -- style.css -- game.js -- wasm_exec.js -- main.wasm -- assets/ (Font-Ordner) - -Server muss WASM mit korrektem MIME-Type ausliefern: -``` -Content-Type: application/wasm -``` diff --git a/cmd/client/web/assets b/cmd/client/web/assets deleted file mode 120000 index ec2e4be..0000000 --- a/cmd/client/web/assets +++ /dev/null @@ -1 +0,0 @@ -../assets \ No newline at end of file diff --git a/cmd/client/assets/assets.json b/cmd/client/web/assets/assets.json similarity index 100% rename from cmd/client/assets/assets.json rename to cmd/client/web/assets/assets.json diff --git a/cmd/client/assets/background.jpg b/cmd/client/web/assets/background.jpg similarity index 100% rename from cmd/client/assets/background.jpg rename to cmd/client/web/assets/background.jpg diff --git a/cmd/client/assets/background1.jpg b/cmd/client/web/assets/background1.jpg similarity index 100% rename from cmd/client/assets/background1.jpg rename to cmd/client/web/assets/background1.jpg diff --git a/cmd/client/assets/background2.jpg b/cmd/client/web/assets/background2.jpg similarity index 100% rename from cmd/client/assets/background2.jpg rename to cmd/client/web/assets/background2.jpg diff --git a/cmd/client/assets/baskeball.png b/cmd/client/web/assets/baskeball.png similarity index 100% rename from cmd/client/assets/baskeball.png rename to cmd/client/web/assets/baskeball.png diff --git a/cmd/client/assets/chunks/chunk_01.json b/cmd/client/web/assets/chunks/chunk_01.json similarity index 100% rename from cmd/client/assets/chunks/chunk_01.json rename to cmd/client/web/assets/chunks/chunk_01.json diff --git a/cmd/client/assets/chunks/chunk_02.json b/cmd/client/web/assets/chunks/chunk_02.json similarity index 100% rename from cmd/client/assets/chunks/chunk_02.json rename to cmd/client/web/assets/chunks/chunk_02.json diff --git a/cmd/client/assets/chunks/chunk_03.json b/cmd/client/web/assets/chunks/chunk_03.json similarity index 100% rename from cmd/client/assets/chunks/chunk_03.json rename to cmd/client/web/assets/chunks/chunk_03.json diff --git a/cmd/client/assets/chunks/chunk_04.json b/cmd/client/web/assets/chunks/chunk_04.json similarity index 100% rename from cmd/client/assets/chunks/chunk_04.json rename to cmd/client/web/assets/chunks/chunk_04.json diff --git a/cmd/client/assets/chunks/start.json b/cmd/client/web/assets/chunks/start.json similarity index 100% rename from cmd/client/assets/chunks/start.json rename to cmd/client/web/assets/chunks/start.json diff --git a/cmd/client/assets/coin.png b/cmd/client/web/assets/coin.png similarity index 100% rename from cmd/client/assets/coin.png rename to cmd/client/web/assets/coin.png diff --git a/cmd/client/assets/desk.png b/cmd/client/web/assets/desk.png similarity index 100% rename from cmd/client/assets/desk.png rename to cmd/client/web/assets/desk.png diff --git a/cmd/client/assets/eraser.png b/cmd/client/web/assets/eraser.png similarity index 100% rename from cmd/client/assets/eraser.png rename to cmd/client/web/assets/eraser.png diff --git a/cmd/client/assets/fonts/press-start-2p-v16-latin-regular.woff2 b/cmd/client/web/assets/fonts/press-start-2p-v16-latin-regular.woff2 similarity index 100% rename from cmd/client/assets/fonts/press-start-2p-v16-latin-regular.woff2 rename to cmd/client/web/assets/fonts/press-start-2p-v16-latin-regular.woff2 diff --git a/cmd/client/assets/front.ttf b/cmd/client/web/assets/front.ttf similarity index 100% rename from cmd/client/assets/front.ttf rename to cmd/client/web/assets/front.ttf diff --git a/cmd/client/assets/g-l.png b/cmd/client/web/assets/g-l.png similarity index 100% rename from cmd/client/assets/g-l.png rename to cmd/client/web/assets/g-l.png diff --git a/cmd/client/assets/game.mp3 b/cmd/client/web/assets/game.mp3 similarity index 100% rename from cmd/client/assets/game.mp3 rename to cmd/client/web/assets/game.mp3 diff --git a/cmd/client/assets/game.wav b/cmd/client/web/assets/game.wav similarity index 100% rename from cmd/client/assets/game.wav rename to cmd/client/web/assets/game.wav diff --git a/cmd/client/assets/gen_plat_1767135546.png b/cmd/client/web/assets/gen_plat_1767135546.png similarity index 100% rename from cmd/client/assets/gen_plat_1767135546.png rename to cmd/client/web/assets/gen_plat_1767135546.png diff --git a/cmd/client/assets/gen_plat_1767369130.png b/cmd/client/web/assets/gen_plat_1767369130.png similarity index 100% rename from cmd/client/assets/gen_plat_1767369130.png rename to cmd/client/web/assets/gen_plat_1767369130.png diff --git a/cmd/client/assets/gen_wall_1767369789.png b/cmd/client/web/assets/gen_wall_1767369789.png similarity index 100% rename from cmd/client/assets/gen_wall_1767369789.png rename to cmd/client/web/assets/gen_wall_1767369789.png diff --git a/cmd/client/assets/godmode.png b/cmd/client/web/assets/godmode.png similarity index 100% rename from cmd/client/assets/godmode.png rename to cmd/client/web/assets/godmode.png diff --git a/cmd/client/assets/h-l.png b/cmd/client/web/assets/h-l.png similarity index 100% rename from cmd/client/assets/h-l.png rename to cmd/client/web/assets/h-l.png diff --git a/cmd/client/assets/jump.wav b/cmd/client/web/assets/jump.wav similarity index 100% rename from cmd/client/assets/jump.wav rename to cmd/client/web/assets/jump.wav diff --git a/cmd/client/assets/jump0.png b/cmd/client/web/assets/jump0.png similarity index 100% rename from cmd/client/assets/jump0.png rename to cmd/client/web/assets/jump0.png diff --git a/cmd/client/assets/jump1.png b/cmd/client/web/assets/jump1.png similarity index 100% rename from cmd/client/assets/jump1.png rename to cmd/client/web/assets/jump1.png diff --git a/cmd/client/assets/jumpboost.png b/cmd/client/web/assets/jumpboost.png similarity index 100% rename from cmd/client/assets/jumpboost.png rename to cmd/client/web/assets/jumpboost.png diff --git a/cmd/client/assets/k-l-monitor.png b/cmd/client/web/assets/k-l-monitor.png similarity index 100% rename from cmd/client/assets/k-l-monitor.png rename to cmd/client/web/assets/k-l-monitor.png diff --git a/cmd/client/assets/k-l.png b/cmd/client/web/assets/k-l.png similarity index 100% rename from cmd/client/assets/k-l.png rename to cmd/client/web/assets/k-l.png diff --git a/cmd/client/assets/k-m.png b/cmd/client/web/assets/k-m.png similarity index 100% rename from cmd/client/assets/k-m.png rename to cmd/client/web/assets/k-m.png diff --git a/cmd/client/assets/m-l.png b/cmd/client/web/assets/m-l.png similarity index 100% rename from cmd/client/assets/m-l.png rename to cmd/client/web/assets/m-l.png diff --git a/cmd/client/assets/p-l.png b/cmd/client/web/assets/p-l.png similarity index 100% rename from cmd/client/assets/p-l.png rename to cmd/client/web/assets/p-l.png diff --git a/cmd/client/assets/pc-trash.png b/cmd/client/web/assets/pc-trash.png similarity index 100% rename from cmd/client/assets/pc-trash.png rename to cmd/client/web/assets/pc-trash.png diff --git a/cmd/client/assets/pickupCoin.wav b/cmd/client/web/assets/pickupCoin.wav similarity index 100% rename from cmd/client/assets/pickupCoin.wav rename to cmd/client/web/assets/pickupCoin.wav diff --git a/cmd/client/assets/platform_1767135546.png b/cmd/client/web/assets/platform_1767135546.png similarity index 100% rename from cmd/client/assets/platform_1767135546.png rename to cmd/client/web/assets/platform_1767135546.png diff --git a/cmd/client/assets/player.png b/cmd/client/web/assets/player.png similarity index 100% rename from cmd/client/assets/player.png rename to cmd/client/web/assets/player.png diff --git a/cmd/client/assets/playernew.png b/cmd/client/web/assets/playernew.png similarity index 100% rename from cmd/client/assets/playernew.png rename to cmd/client/web/assets/playernew.png diff --git a/cmd/client/assets/powerUp.wav b/cmd/client/web/assets/powerUp.wav similarity index 100% rename from cmd/client/assets/powerUp.wav rename to cmd/client/web/assets/powerUp.wav diff --git a/cmd/client/assets/r-l.png b/cmd/client/web/assets/r-l.png similarity index 100% rename from cmd/client/assets/r-l.png rename to cmd/client/web/assets/r-l.png diff --git a/cmd/client/assets/t-s.png b/cmd/client/web/assets/t-s.png similarity index 100% rename from cmd/client/assets/t-s.png rename to cmd/client/web/assets/t-s.png diff --git a/cmd/client/assets/w-l.png b/cmd/client/web/assets/w-l.png similarity index 100% rename from cmd/client/assets/w-l.png rename to cmd/client/web/assets/w-l.png diff --git a/cmd/client/web/background.jpg b/cmd/client/web/background.jpg new file mode 100644 index 0000000..dc6a800 Binary files /dev/null and b/cmd/client/web/background.jpg differ diff --git a/cmd/client/web/game.js b/cmd/client/web/game.js index 9cc397c..0807719 100644 --- a/cmd/client/web/game.js +++ b/cmd/client/web/game.js @@ -2,6 +2,130 @@ let wasmReady = false; let gameStarted = false; let audioMuted = false; +let currentLeaderboard = []; // Store full leaderboard data with proof codes + +// UI State Management - Single Source of Truth +const UIState = { + LOADING: 'loading', + MENU: 'menu', + LOBBY: 'lobby', + PLAYING: 'playing', + GAME_OVER: 'gameover', + LEADERBOARD: 'leaderboard', + SETTINGS: 'settings', + COOP_MENU: 'coop_menu', + MY_CODES: 'mycodes', + IMPRESSUM: 'impressum', + DATENSCHUTZ: 'datenschutz' +}; + +let currentUIState = UIState.LOADING; + +// Central UI State Manager +function setUIState(newState) { + console.log('🎨 UI State:', currentUIState, '->', newState); + currentUIState = newState; + + const canvas = document.querySelector('canvas'); + const loadingScreen = document.getElementById('loading'); + + // Hide all overlays first + document.querySelectorAll('.overlay-screen').forEach(screen => { + screen.classList.add('hidden'); + }); + if (loadingScreen) loadingScreen.style.display = 'none'; + + // Manage Canvas and Overlays based on state + switch(newState) { + case UIState.LOADING: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + if (loadingScreen) loadingScreen.style.display = 'flex'; + break; + + case UIState.MENU: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('menu').classList.remove('hidden'); + break; + + case UIState.LOBBY: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('lobbyScreen').classList.remove('hidden'); + break; + + case UIState.PLAYING: + if (canvas) { + canvas.classList.add('game-active'); + canvas.style.visibility = 'visible'; + } + // No overlays shown during gameplay + break; + + case UIState.GAME_OVER: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('gameOverScreen').classList.remove('hidden'); + break; + + case UIState.LEADERBOARD: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('leaderboardMenu').classList.remove('hidden'); + break; + + case UIState.SETTINGS: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('settingsMenu').classList.remove('hidden'); + break; + + case UIState.COOP_MENU: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('coopMenu').classList.remove('hidden'); + break; + + case UIState.MY_CODES: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('myCodesMenu').classList.remove('hidden'); + break; + + case UIState.IMPRESSUM: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('impressumMenu').classList.remove('hidden'); + break; + + case UIState.DATENSCHUTZ: + if (canvas) { + canvas.classList.remove('game-active'); + canvas.style.visibility = 'hidden'; + } + document.getElementById('datenschutzMenu').classList.remove('hidden'); + break; + } +} // WebSocket for Leaderboard (direct JS connection) let leaderboardWS = null; @@ -89,8 +213,11 @@ async function initWASM() { go.run(result.instance); wasmReady = true; - // Hide loading screen - document.getElementById('loading').style.display = 'none'; + // Switch to menu state + setUIState(UIState.MENU); + + // Enable all start buttons + enableStartButtons(); console.log('✅ WASM loaded successfully'); @@ -104,53 +231,32 @@ async function initWASM() { } } -// Menu Navigation -function showMainMenu() { - hideAllScreens(); - document.getElementById('menu').classList.remove('hidden'); -} - -function showCoopMenu() { - hideAllScreens(); - document.getElementById('coopMenu').classList.remove('hidden'); -} - -function showSettings() { - hideAllScreens(); - document.getElementById('settingsMenu').classList.remove('hidden'); -} - -function showLeaderboard() { - hideAllScreens(); - document.getElementById('leaderboardMenu').classList.remove('hidden'); - loadLeaderboard(); -} - -function hideAllScreens() { - document.querySelectorAll('.overlay-screen').forEach(screen => { - screen.classList.add('hidden'); +// Enable start buttons after WASM is ready +function enableStartButtons() { + const buttons = ['startBtn', 'coopBtn', 'createRoomBtn', 'joinRoomBtn']; + buttons.forEach(btnId => { + const btn = document.getElementById(btnId); + if (btn) { + btn.disabled = false; + btn.style.opacity = '1'; + btn.style.cursor = 'pointer'; + } }); + console.log('✅ Start-Buttons aktiviert (Solo + Coop)'); } -function hideMenu() { - document.getElementById('menu').style.display = 'none'; - // Canvas sichtbar machen für Gameplay - const canvas = document.querySelector('canvas'); - if (canvas) { - canvas.classList.add('game-active'); - } -} - -function showMenu() { - document.getElementById('menu').style.display = 'flex'; - document.getElementById('menu').classList.remove('hidden'); - showMainMenu(); - // Canvas verstecken im Menü - const canvas = document.querySelector('canvas'); - if (canvas) { - canvas.classList.remove('game-active'); - } -} +// Menu Navigation +// Legacy function wrappers - use setUIState instead +function showMainMenu() { setUIState(UIState.MENU); } +function showCoopMenu() { setUIState(UIState.COOP_MENU); } +function showSettings() { setUIState(UIState.SETTINGS); } +function showLeaderboard() { setUIState(UIState.LEADERBOARD); loadLeaderboard(); } +function showMyCodes() { setUIState(UIState.MY_CODES); loadMyCodes(); } +function showImpressum() { setUIState(UIState.IMPRESSUM); } +function showDatenschutz() { setUIState(UIState.DATENSCHUTZ); } +function hideAllScreens() { /* Handled by setUIState */ } +function hideMenu() { /* Handled by setUIState */ } +function showMenu() { setUIState(UIState.MENU); } // Game Functions function startSoloGame() { @@ -166,23 +272,17 @@ function startSoloGame() { localStorage.setItem('escape_game_mode', 'solo'); localStorage.setItem('escape_room_id', ''); - // Hide ALL screens including main menu - hideAllScreens(); - document.getElementById('menu').style.display = 'none'; gameStarted = true; - // Canvas sichtbar machen - const canvas = document.querySelector('canvas'); - if (canvas) { - canvas.classList.add('game-active'); - } + // Don't switch UI state yet - wait for WASM callback onGameStarted() + // The server will auto-start solo games after 2 seconds // Trigger WASM game start if (window.startGame) { window.startGame('solo', playerName, ''); } - console.log('🎮 Solo game started:', playerName); + console.log('🎮 Solo game starting - waiting for server auto-start...'); } function createRoom() { @@ -202,12 +302,8 @@ function createRoom() { localStorage.setItem('escape_team_name', teamName); localStorage.setItem('escape_is_host', 'true'); - // Verstecke ALLE Screens inkl. Hauptmenü - hideAllScreens(); - document.getElementById('menu').style.display = 'none'; - - // Zeige HTML Lobby Screen - document.getElementById('lobbyScreen').classList.remove('hidden'); + // Show Lobby + setUIState(UIState.LOBBY); document.getElementById('lobbyRoomCode').textContent = roomID; document.getElementById('lobbyHostControls').classList.remove('hidden'); document.getElementById('lobbyStatus').textContent = 'Du bist Host - starte wenn bereit!'; @@ -242,12 +338,8 @@ function joinRoom() { localStorage.setItem('escape_team_name', teamName); localStorage.setItem('escape_is_host', 'false'); - // Verstecke ALLE Screens inkl. Hauptmenü - hideAllScreens(); - document.getElementById('menu').style.display = 'none'; - - // Zeige HTML Lobby Screen - document.getElementById('lobbyScreen').classList.remove('hidden'); + // Show Lobby + setUIState(UIState.LOBBY); document.getElementById('lobbyRoomCode').textContent = roomID; document.getElementById('lobbyHostControls').classList.add('hidden'); document.getElementById('lobbyStatus').textContent = 'Warte auf Host...'; @@ -291,6 +383,42 @@ function updateLobbyPlayers(players) { console.log('👥 Lobby players updated:', players.length); } +// Update Lobby Team Name (called by WASM) +function updateLobbyTeamName(teamName, isHost) { + const teamNameBox = document.getElementById('lobbyTeamNameBox'); + const teamNameDisplay = document.getElementById('teamNameDisplay'); + + // Zeige Team-Name Box nur für Host + if (isHost) { + teamNameBox.classList.remove('hidden'); + + // Setup Event Listener für Input-Feld (nur einmal) + const input = document.getElementById('lobbyTeamName'); + if (!input.dataset.listenerAdded) { + input.addEventListener('input', function() { + const newTeamName = this.value.toUpperCase().trim(); + if (newTeamName && window.setTeamName_WASM) { + window.setTeamName_WASM(newTeamName); + } + }); + input.dataset.listenerAdded = 'true'; + } + } else { + teamNameBox.classList.add('hidden'); + } + + // Aktualisiere Team-Name Anzeige + if (teamName && teamName !== '') { + teamNameDisplay.textContent = teamName; + teamNameDisplay.style.color = '#00ff00'; + } else { + teamNameDisplay.textContent = 'Nicht gesetzt'; + teamNameDisplay.style.color = '#888'; + } + + console.log('🏷️ Team name updated:', teamName, 'isHost:', isHost); +} + function loadLeaderboard() { const list = document.getElementById('leaderboardList'); list.innerHTML = '
Lädt Leaderboard...
'; @@ -308,6 +436,9 @@ function loadLeaderboard() { // Called by WASM to update leaderboard function updateLeaderboard(entries) { + // Store full leaderboard data globally + currentLeaderboard = entries || []; + // Update ALL leaderboard displays const list = document.getElementById('leaderboardList'); const startList = document.getElementById('startLeaderboardList'); @@ -423,8 +554,7 @@ document.addEventListener('keydown', (e) => { // Show Game Over Screen (called by WASM) function showGameOver(score) { - hideAllScreens(); - document.getElementById('gameOverScreen').classList.remove('hidden'); + setUIState(UIState.GAME_OVER); document.getElementById('finalScore').textContent = score; // Update local highscore @@ -436,29 +566,137 @@ function showGameOver(score) { // Request leaderboard via direct WebSocket requestLeaderboardDirect(); - console.log('💀 Game Over! Score:', score); + // Note: Proof-Code wird jetzt direkt vom Server über score_response WebSocket Nachricht gesendet + // und von WASM (connection_wasm.go) automatisch an saveHighscoreCode() weitergeleitet + + console.log('💀 Game Over! Score:', score, '- Warte auf Proof-Code vom Server...'); } // Called by WASM when game actually starts function onGameStarted() { console.log('🎮 Game Started - Making canvas visible'); - hideAllScreens(); - document.getElementById('menu').style.display = 'none'; gameStarted = true; + setUIState(UIState.PLAYING); +} - // Canvas sichtbar machen - const canvas = document.querySelector('canvas'); - if (canvas) { - canvas.classList.add('game-active'); +// ===== MY CODES MANAGEMENT ===== + +// Save highscore code to localStorage +function saveHighscoreCode(score, proofCode, playerName) { + const codes = getMySavedCodes(); + const newCode = { + score: score, + proof: proofCode, + player_name: playerName, + timestamp: Date.now(), + date: new Date().toLocaleString('de-DE') + }; + codes.push(newCode); + localStorage.setItem('escape_highscore_codes', JSON.stringify(codes)); + console.log('💾 Highscore-Code gespeichert:', proofCode, 'für Score:', score); +} + +// Get all saved codes from localStorage +function getMySavedCodes() { + const stored = localStorage.getItem('escape_highscore_codes'); + if (!stored) return []; + try { + return JSON.parse(stored); + } catch (e) { + return []; } } +// Load and display my codes +function loadMyCodes() { + const codes = getMySavedCodes(); + const list = document.getElementById('myCodesList'); + + if (codes.length === 0) { + list.innerHTML = '
Noch keine Highscores erreicht!
'; + return; + } + + // Sort by score descending + codes.sort((a, b) => b.score - a.score); + + // Request current leaderboard to check positions + requestLeaderboardForCodes(codes); +} + +// Request leaderboard and then display codes with positions +function requestLeaderboardForCodes(codes) { + const list = document.getElementById('myCodesList'); + list.innerHTML = '
Lade Positionen...
'; + + // Use the direct leaderboard WebSocket + requestLeaderboardDirect(); + + // Wait a bit for leaderboard to arrive, then display + setTimeout(() => { + displayMyCodesWithPositions(codes); + }, 1000); +} + +// Display codes with their leaderboard positions +function displayMyCodesWithPositions(codes) { + const list = document.getElementById('myCodesList'); + let html = ''; + + codes.forEach((code, index) => { + // Try to find position in current leaderboard + const position = findPositionInLeaderboard(code.proof); + const positionText = position > 0 ? `#${position}` : 'Nicht in Top 10'; + const positionColor = position > 0 ? '#fc0' : '#888'; + + html += ` +
+
+
+ ${code.score} Punkte + ${positionText} +
+ +
+
+
Name: ${code.player_name}
+
Code: ${code.proof}
+
Datum: ${code.date}
+
+
+ `; + }); + + list.innerHTML = html; +} + +// Find position of a proof code in the current leaderboard +function findPositionInLeaderboard(proofCode) { + if (!currentLeaderboard || currentLeaderboard.length === 0) return -1; + + // Find the entry with matching proof code + const index = currentLeaderboard.findIndex(entry => entry.proof_code === proofCode); + return index >= 0 ? index + 1 : -1; // Return 1-based position +} + +// Delete a highscore code +function deleteHighscoreCode(index) { + if (!confirm('Diesen Highscore-Code wirklich löschen?')) return; + + const codes = getMySavedCodes(); + codes.splice(index, 1); + localStorage.setItem('escape_highscore_codes', JSON.stringify(codes)); + console.log('🗑️ Highscore-Code gelöscht'); + loadMyCodes(); // Reload display +} + // Export functions for WASM to call window.showMenu = showMenu; window.hideMenu = hideMenu; window.updateLeaderboard = updateLeaderboard; window.showGameOver = showGameOver; window.onGameStarted = onGameStarted; +window.saveHighscoreCode = saveHighscoreCode; // Initialize on load initWASM(); diff --git a/cmd/client/web/index.html b/cmd/client/web/index.html index c164aa6..dbd3f1d 100644 --- a/cmd/client/web/index.html +++ b/cmd/client/web/index.html @@ -27,8 +27,8 @@ - - + +
SCHUL-NEWS
@@ -49,8 +49,14 @@ + +
@@ -67,13 +73,13 @@

CO-OP MODUS

- +
- ODER -
- +
@@ -115,6 +121,112 @@
+ + + + + + + + + + + diff --git a/cmd/client/web/main.wasm b/cmd/client/web/main.wasm index 6d06ab2..6c05071 100755 Binary files a/cmd/client/web/main.wasm and b/cmd/client/web/main.wasm differ diff --git a/cmd/client/web/pc-trash.png b/cmd/client/web/pc-trash.png deleted file mode 100644 index 3335dde..0000000 Binary files a/cmd/client/web/pc-trash.png and /dev/null differ diff --git a/cmd/client/web/style.css b/cmd/client/web/style.css index 4f750fc..1daefa6 100644 --- a/cmd/client/web/style.css +++ b/cmd/client/web/style.css @@ -1,9 +1,9 @@ @font-face{font-display:swap;font-family:'Press Start 2P';font-style:normal;font-weight:400;src:url('../assets/fonts/press-start-2p-v16-latin-regular.woff2') format('woff2')} body,html{margin:0;padding:0;width:100%;height:100%;background-color:#1a1a1a;color:#fff;overflow:hidden;font-family:'Press Start 2P',cursive;font-size:14px} #game-container{position:relative;width:100%;height:100%;box-shadow:0 0 50px rgba(0,0,0,.8);border:4px solid #444;background:#000} -canvas{position:fixed!important;top:0!important;left:0!important;width:100%!important;height:100%!important;z-index:1!important;background:#000;image-rendering:pixelated;opacity:0;pointer-events:none;transition:opacity .3s} -canvas.game-active{opacity:1;pointer-events:auto;z-index:2000!important} -.overlay-screen{position:fixed!important;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.95);display:flex;justify-content:center;align-items:center;z-index:1000;box-sizing:border-box;padding:20px} +canvas{position:fixed!important;top:0!important;left:0!important;width:100%!important;height:100%!important;z-index:1!important;background:#000;image-rendering:pixelated;opacity:0;pointer-events:none;transition:opacity .3s;visibility:hidden} +canvas.game-active{opacity:1;pointer-events:auto;z-index:2000!important;visibility:visible} +.overlay-screen{position:fixed!important;top:0;left:0;width:100%;height:100%;background:url('background.jpg') center/cover no-repeat,rgba(0,0,0,.85);display:flex;justify-content:center;align-items:center;z-index:1000;box-sizing:border-box;padding:20px} .overlay-screen.hidden{display:none!important} #startScreen{display:flex;flex-direction:row;gap:40px;width:100%;height:100%;align-items:center;justify-content:center} .start-left{flex:2;display:flex;flex-direction:column;align-items:center;justify-content:center;max-width:60%} @@ -14,15 +14,15 @@ button{font-family:'Press Start 2P',cursive;background:#fc0;border:4px solid #ff button:hover{background:#ffd700} button:active{transform:translateY(4px);box-shadow:0 1px 0 #997a00} .big-btn{font-size:22px;padding:20px 40px} -.back-btn{background:0 0;border:2px solid #666;color:#888;box-shadow:none;font-size:12px;padding:10px 20px;margin-top:30px} -.back-btn:hover{background:#333;color:#fff;border-color:#fff} -.legal-btn{font-size:10px;padding:8px 12px;margin:5px;background:0 0;border:1px solid #666;color:#888;box-shadow:none} -.legal-btn:hover{background:#333;color:#fff;border-color:#fff} +.back-btn{background:#fc0;border:3px solid #fff;color:#000;box-shadow:0 4px 0 #997a00;font-size:12px;padding:10px 20px;margin-top:30px;font-weight:700} +.back-btn:hover{background:#ffd700;color:#000;transform:translateY(2px);box-shadow:0 2px 0 #997a00} +.legal-btn{font-size:10px;padding:8px 12px;margin:5px;background:rgba(0,0,0,.6);border:2px solid #fc0;color:#fc0;box-shadow:none} +.legal-btn:hover{background:rgba(255,204,0,.2);color:#fff;border-color:#fff} input[type=text]{font-family:'Press Start 2P',cursive;padding:12px;font-size:16px;border:3px solid #fff;background:#222;color:#fff;text-align:center;margin-bottom:15px;width:100%;max-width:350px;outline:0;box-sizing:border-box} input[type=text]::placeholder{color:#666} input[type=text]:focus{border-color:#fc0;box-shadow:0 0 10px rgba(255,204,0,.5)} input[type=range]{width:100%;max-width:300px} -.info-box{background:rgba(255,255,255,.1);border:2px solid #555;padding:12px;margin:8px 0;width:100%;max-width:320px;text-align:left;box-sizing:border-box} +.info-box{background:rgba(0,0,0,.6);border:4px solid #fc0;padding:15px;margin:8px 0;width:100%;max-width:320px;text-align:left;box-sizing:border-box} .info-box p{font-family:sans-serif;font-size:14px;color:#ccc;line-height:1.4;margin:0} .info-title{color:#fc0;font-size:12px;margin-bottom:6px;text-align:center;text-decoration:underline} .legal-bar{margin-top:20px;display:flex;gap:15px;flex-wrap:wrap;justify-content:center} diff --git a/cmd/client/web/test_visibility.html b/cmd/client/web/test_visibility.html deleted file mode 100644 index 909b45c..0000000 --- a/cmd/client/web/test_visibility.html +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - -
TEST - SIEHST DU MICH?
- - diff --git a/cmd/levelbuilder/main.go b/cmd/levelbuilder/main.go index 9b7cc64..26d8832 100644 --- a/cmd/levelbuilder/main.go +++ b/cmd/levelbuilder/main.go @@ -25,8 +25,8 @@ import ( // --- CONFIG --- const ( - AssetFile = "./cmd/client/assets/assets.json" - ChunkDir = "./cmd/client/assets/chunks" + AssetFile = "cmd/client/web/assets/assets.json" + ChunkDir = "cmd/client/web/assets/chunks" LeftSidebarWidth = 250 RightSidebarWidth = 250 @@ -116,7 +116,7 @@ func (le *LevelEditor) LoadAssets() { json.Unmarshal(data, &le.assetManifest) } - baseDir := "./cmd/client/assets" + baseDir := "./cmd/client/web/assets" le.assetList = []string{} for id, def := range le.assetManifest.Assets { diff --git a/cmd/server/main.go b/cmd/server/main.go index 0d1ada4..927d763 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -93,7 +93,25 @@ func main() { log.Printf("🔍 RAW NATS: Nachricht empfangen auf game.join: %s", string(m.Data)) }) - // 4. HANDLER: INPUT + // 4. HANDLER: GAME START + _, _ = ec.Subscribe("game.start", func(req *game.StartRequest) { + log.Printf("▶️ START empfangen: RoomID=%s", req.RoomID) + + mu.RLock() + room, exists := rooms[req.RoomID] + mu.RUnlock() + + if exists { + room.Mutex.Lock() + room.StartCountdown() + room.Mutex.Unlock() + log.Printf("🎮 Raum '%s' Countdown gestartet", req.RoomID) + } else { + log.Printf("❌ Raum '%s' nicht gefunden", req.RoomID) + } + }) + + // 5. HANDLER: INPUT _, _ = ec.Subscribe("game.input", func(input *game.ClientInput) { mu.RLock() room, ok := playerSessions[input.PlayerID] @@ -104,23 +122,40 @@ func main() { } }) - // 5. HANDLER: SCORE SUBMISSION + // 6. HANDLER: SCORE SUBMISSION _, _ = ec.Subscribe("score.submit", func(submission *game.ScoreSubmission) { - log.Printf("📊 Score-Submission: %s (%s) mit %d Punkten", submission.PlayerName, submission.PlayerCode, submission.Score) - added := server.GlobalLeaderboard.AddScore(submission.PlayerName, submission.PlayerCode, submission.Score) + // Verwende Team-Name wenn vorhanden (Coop-Mode), sonst Player-Name (Solo-Mode) + displayName := submission.PlayerName + if submission.TeamName != "" { + displayName = submission.TeamName + } + + log.Printf("📊 Score-Submission: %s (%s) mit %d Punkten [Mode: %s]", displayName, submission.PlayerCode, submission.Score, submission.Mode) + added, proofCode := server.GlobalLeaderboard.AddScore(displayName, submission.PlayerCode, submission.Score) if added { - log.Printf("✅ Score akzeptiert für %s", submission.PlayerName) + log.Printf("✅ Score akzeptiert für %s (Proof: %s)", displayName, proofCode) + + // Sende Response zurück über NATS + response := game.ScoreSubmissionResponse{ + Success: true, + ProofCode: proofCode, + Score: submission.Score, + } + // Sende an player-spezifischen Channel + channel := "score.response." + submission.PlayerCode + ec.Publish(channel, &response) + log.Printf("📤 Proof-Code gesendet an Channel: %s", channel) } }) - // 6. HANDLER: LEADERBOARD REQUEST (alt, für Kompatibilität) + // 7. HANDLER: LEADERBOARD REQUEST (alt, für Kompatibilität) _, _ = ec.Subscribe("leaderboard.get", func(subject, reply string, _ *struct{}) { top10 := server.GlobalLeaderboard.GetTop10() log.Printf("📊 Leaderboard-Request beantwortet: %d Einträge", len(top10)) ec.Publish(reply, top10) }) - // 7. HANDLER: LEADERBOARD REQUEST (neu, für WebSocket-Gateway) + // 8. HANDLER: LEADERBOARD REQUEST (neu, für WebSocket-Gateway) _, _ = ec.Subscribe("leaderboard.request", func(req *game.LeaderboardRequest) { top10 := server.GlobalLeaderboard.GetTop10() log.Printf("📊 Leaderboard-Request (Mode=%s): %d Einträge", req.Mode, len(top10)) @@ -137,7 +172,7 @@ func main() { log.Println("✅ Server bereit. Warte auf Spieler...") - // 5. WEBSOCKET-GATEWAY STARTEN (für Browser-Clients) + // 9. WEBSOCKET-GATEWAY STARTEN (für Browser-Clients) go StartWebSocketGateway("8080", ec) // Block forever @@ -145,7 +180,7 @@ func main() { } func loadServerAssets(w *game.World) { - assetDir := "./cmd/client/assets" + assetDir := "./cmd/client/web/assets" chunkDir := filepath.Join(assetDir, "chunks") // Manifest laden diff --git a/cmd/server/websocket_gateway.go b/cmd/server/websocket_gateway.go index 8559249..e5acf1e 100644 --- a/cmd/server/websocket_gateway.go +++ b/cmd/server/websocket_gateway.go @@ -26,13 +26,15 @@ type WebSocketMessage struct { // WebSocketClient repräsentiert einen verbundenen WebSocket-Client type WebSocketClient struct { - conn *websocket.Conn - natsConn *nats.EncodedConn - playerID string - roomID string - send chan []byte - mutex sync.Mutex - subUpdates *nats.Subscription + conn *websocket.Conn + natsConn *nats.EncodedConn + playerID string + playerCode string + roomID string + send chan []byte + mutex sync.Mutex + subUpdates *nats.Subscription + subScoreResp *nats.Subscription } // handleWebSocket verwaltet eine WebSocket-Verbindung @@ -62,6 +64,9 @@ func (c *WebSocketClient) readPump() { if c.subUpdates != nil { c.subUpdates.Unsubscribe() } + if c.subScoreResp != nil { + c.subScoreResp.Unsubscribe() + } c.conn.Close() log.Printf("🔌 WebSocket-Client getrennt: %s", c.conn.RemoteAddr()) }() @@ -208,6 +213,35 @@ func (c *WebSocketClient) handleMessage(msg WebSocketMessage) { } log.Printf("📊 WebSocket Score-Submit: Player=%s, Score=%d", submit.PlayerCode, submit.Score) + + // Speichere PlayerCode und subscribe auf Response-Channel + if c.playerCode == "" && submit.PlayerCode != "" { + c.playerCode = submit.PlayerCode + + // Subscribe auf Score-Response für diesen Spieler + responseChannel := "score.response." + submit.PlayerCode + sub, err := c.natsConn.Subscribe(responseChannel, func(resp *game.ScoreSubmissionResponse) { + // ScoreSubmissionResponse an WebSocket-Client senden + data, _ := json.Marshal(map[string]interface{}{ + "type": "score_response", + "payload": resp, + }) + select { + case c.send <- data: + log.Printf("📤 Proof-Code an Client gesendet: %s", resp.ProofCode) + default: + log.Printf("⚠️ Send channel voll, Proof-Code verworfen") + } + }) + + if err != nil { + log.Printf("❌ Fehler beim Subscribe auf %s: %v", responseChannel, err) + } else { + c.subScoreResp = sub + log.Printf("👂 WebSocket-Client lauscht auf Score-Responses: %s", responseChannel) + } + } + c.natsConn.Publish("score.submit", &submit) default: diff --git a/pkg/config/config.go b/pkg/config/config.go index de2ae30..4bcf1dc 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,8 +5,8 @@ import "time" const ( // Server Settings Port = ":8080" - AssetPath = "./cmd/client/assets/assets.json" - ChunkDir = "./cmd/client/assets/chunks" + AssetPath = "./cmd/client/web/assets/assets.json" + ChunkDir = "./cmd/client/web/assets/chunks" // Physics Gravity = 0.5 diff --git a/pkg/game/data.go b/pkg/game/data.go index 9e0b417..9a110e1 100644 --- a/pkg/game/data.go +++ b/pkg/game/data.go @@ -66,10 +66,11 @@ type LoginPayload struct { // Input vom Spieler während des Spiels type ClientInput struct { - Type string `json:"type"` // "JUMP", "START", "LEFT_DOWN", "RIGHT_DOWN", etc. + Type string `json:"type"` // "JUMP", "START", "LEFT_DOWN", "RIGHT_DOWN", "SET_TEAM_NAME", etc. RoomID string `json:"room_id"` PlayerID string `json:"player_id"` - Sequence uint32 `json:"sequence"` // Sequenznummer für Client Prediction + Sequence uint32 `json:"sequence"` // Sequenznummer für Client Prediction + TeamName string `json:"team_name,omitempty"` // Für SET_TEAM_NAME Input } type JoinRequest struct { @@ -105,6 +106,7 @@ type GameState struct { TimeLeft int `json:"time_left"` WorldChunks []ActiveChunk `json:"world_chunks"` HostID string `json:"host_id"` + TeamName string `json:"team_name"` // Team-Name (vom Host gesetzt) ScrollX float64 `json:"scroll_x"` CollectedCoins map[string]bool `json:"collected_coins"` // Welche Coins wurden eingesammelt (Key: ChunkID_ObjectIndex) CollectedPowerups map[string]bool `json:"collected_powerups"` // Welche Powerups wurden eingesammelt @@ -125,7 +127,8 @@ type LeaderboardEntry struct { PlayerName string `json:"player_name"` PlayerCode string `json:"player_code"` // Eindeutiger Code für Verifikation Score int `json:"score"` - Timestamp int64 `json:"timestamp"` // Unix-Timestamp + Timestamp int64 `json:"timestamp"` // Unix-Timestamp + ProofCode string `json:"proof_code"` // Beweis-Code zum Verifizieren des Scores } // Score-Submission vom Client an Server @@ -133,8 +136,16 @@ type ScoreSubmission struct { PlayerName string `json:"player_name"` PlayerCode string `json:"player_code"` Score int `json:"score"` - Name string `json:"name"` // Alternativer Name-Feld (für Kompatibilität) - Mode string `json:"mode"` // "solo" oder "coop" + Name string `json:"name"` // Alternativer Name-Feld (für Kompatibilität) + Mode string `json:"mode"` // "solo" oder "coop" + TeamName string `json:"team_name"` // Team-Name für Coop-Mode +} + +// Score-Submission Response vom Server an Client +type ScoreSubmissionResponse struct { + Success bool `json:"success"` + ProofCode string `json:"proof_code"` + Score int `json:"score"` } // Start-Request vom Client diff --git a/pkg/game/world.go b/pkg/game/world.go index d74d6f0..8633e13 100644 --- a/pkg/game/world.go +++ b/pkg/game/world.go @@ -83,7 +83,7 @@ func (w *World) GenerateColliders(activeChunks []ActiveChunk) []Collider { continue } - if def.Type == "obstacle" || def.Type == "platform" { + if def.Type == "obstacle" || def.Type == "platform" || def.Type == "wall" { c := Collider{ Rect: Rect{ OffsetX: ac.X + obj.X + def.DrawOffX + def.Hitbox.OffsetX, diff --git a/pkg/server/leaderboard.go b/pkg/server/leaderboard.go index 50753ff..84ac3ca 100644 --- a/pkg/server/leaderboard.go +++ b/pkg/server/leaderboard.go @@ -2,7 +2,10 @@ package server import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" + "fmt" "log" "time" @@ -39,17 +42,31 @@ func InitLeaderboard(redisAddr string) error { return nil } -func (lb *Leaderboard) AddScore(name, code string, score int) bool { +// GenerateProofCode erstellt einen kryptografisch sicheren Proof-Code +func GenerateProofCode(playerCode string, score int, timestamp int64) string { + // Secret Salt (sollte eigentlich aus Config kommen, aber für Demo hier hardcoded) + secret := "EscapeFromTeacher_Secret_2026" + data := fmt.Sprintf("%s:%d:%d:%s", playerCode, score, timestamp, secret) + hash := sha256.Sum256([]byte(data)) + // Nehme erste 12 Zeichen des Hex-Hash + return hex.EncodeToString(hash[:])[:12] +} + +func (lb *Leaderboard) AddScore(name, code string, score int) (bool, string) { // Erstelle eindeutigen Key für diesen Score: PlayerCode + Timestamp timestamp := time.Now().Unix() uniqueKey := code + "_" + time.Now().Format("20060102_150405") + // Generiere Proof-Code + proofCode := GenerateProofCode(code, score, timestamp) + // Score speichern entry := game.LeaderboardEntry{ PlayerName: name, PlayerCode: code, Score: score, Timestamp: timestamp, + ProofCode: proofCode, } data, _ := json.Marshal(entry) @@ -61,8 +78,8 @@ func (lb *Leaderboard) AddScore(name, code string, score int) bool { Member: uniqueKey, }) - log.Printf("🏆 Leaderboard: %s mit %d Punkten (Entry: %s)", name, score, uniqueKey) - return true + log.Printf("🏆 Leaderboard: %s mit %d Punkten (Entry: %s, Proof: %s)", name, score, uniqueKey, proofCode) + return true, proofCode } func (lb *Leaderboard) GetTop10() []game.LeaderboardEntry { diff --git a/pkg/server/room.go b/pkg/server/room.go index 5b45118..8705e5d 100644 --- a/pkg/server/room.go +++ b/pkg/server/room.go @@ -23,6 +23,8 @@ type ServerPlayer struct { InputX float64 // -1 (Links), 0, 1 (Rechts) LastInputSeq uint32 // Letzte verarbeitete Input-Sequenz Score int + DistanceScore int // Score basierend auf zurückgelegter Distanz + BonusScore int // Score aus Coins und anderen Boni IsAlive bool IsSpectator bool @@ -68,9 +70,12 @@ type Room struct { Countdown int NextStart time.Time HostID string + TeamName string // Name des Teams (vom Host gesetzt) CollectedCoins map[string]bool // Key: "chunkID_objectIndex" CollectedPowerups map[string]bool // Key: "chunkID_objectIndex" ScoreAccum float64 // Akkumulator für Distanz-Score + CurrentSpeed float64 // Aktuelle Geschwindigkeit (steigt mit der Zeit) + GameStartTime time.Time // Wann das Spiel gestartet wurde // Chunk-Pool für fairen Random-Spawn ChunkPool []string // Verfügbare Chunks für nächsten Spawn @@ -100,7 +105,8 @@ func NewRoom(id string, nc *nats.Conn, w *game.World) *Room { CollectedCoins: make(map[string]bool), CollectedPowerups: make(map[string]bool), ChunkSpawnedCount: make(map[string]int), - pW: 40, pH: 60, // Fallback + CurrentSpeed: config.RunSpeed, // Startet mit normaler Geschwindigkeit + pW: 40, pH: 60, // Fallback } // Initialisiere Chunk-Pool mit allen verfügbaren Chunks @@ -281,6 +287,12 @@ func (r *Room) HandleInput(input game.ClientInput) { if input.PlayerID == r.HostID && r.Status == "LOBBY" { r.StartCountdown() } + case "SET_TEAM_NAME": + // Nur Host darf Team-Name setzen und nur in der Lobby + if input.PlayerID == r.HostID && r.Status == "LOBBY" { + r.TeamName = input.TeamName + log.Printf("🏷️ Team-Name gesetzt: '%s' (von Host %s)", r.TeamName, p.Name) + } } } @@ -301,9 +313,19 @@ func (r *Room) Update() { r.Countdown = int(rem.Seconds()) + 1 if rem <= 0 { r.Status = "RUNNING" + r.GameStartTime = time.Now() + r.CurrentSpeed = config.RunSpeed } } else if r.Status == "RUNNING" { - r.GlobalScrollX += config.RunSpeed + // Geschwindigkeit erhöhen: +0.5 pro 10 Sekunden (max +5.0 nach 100 Sekunden) + elapsed := time.Since(r.GameStartTime).Seconds() + speedIncrease := (elapsed / 10.0) * 0.5 + if speedIncrease > 5.0 { + speedIncrease = 5.0 + } + r.CurrentSpeed = config.RunSpeed + speedIncrease + + r.GlobalScrollX += r.CurrentSpeed // Bewegende Plattformen updaten r.UpdateMovingPlatforms() } @@ -333,9 +355,9 @@ func (r *Room) Update() { // X Bewegung // Symmetrische Geschwindigkeit: Links = Rechts - // Nach rechts: RunSpeed + 11, Nach links: RunSpeed - 11 - // Ergebnis: Rechts = 18, Links = -4 (beide gleich weit vom Scroll) - currentSpeed := config.RunSpeed + (p.InputX * 11.0) + // Nach rechts: CurrentSpeed + 11, Nach links: CurrentSpeed - 11 + // Verwendet r.CurrentSpeed statt config.RunSpeed für dynamische Geschwindigkeit + currentSpeed := r.CurrentSpeed + (p.InputX * 11.0) nextX := p.X + currentSpeed hitX, typeX := r.CheckCollision(nextX+r.pDrawOffX+r.pHitboxOffX, p.Y+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH) @@ -343,7 +365,7 @@ func (r *Room) Update() { if typeX == "wall" { // Wand getroffen - kann klettern! p.OnWall = true - // X-Position nicht ändern (bleibt an der Wand) + // X-Position NICHT ändern (bleibt vor der Wand stehen) } else if typeX == "obstacle" { // Godmode prüfen if p.HasGodMode && time.Now().Before(p.GodModeEndTime) { @@ -405,15 +427,9 @@ func (r *Room) Update() { hitY, typeY := r.CheckCollision(p.X+r.pDrawOffX+r.pHitboxOffX, nextY+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH) if hitY { if typeY == "wall" { - // An der Wand: Nicht töten, sondern Position halten - if p.OnWall { - p.VY = 0 - } else { - // Von oben/unten gegen Wand - töten (kein Klettern in Y-Richtung) - p.Y = nextY - r.KillPlayer(p) - continue - } + // An der Wand: Nicht töten, Position halten und klettern ermöglichen + p.VY = 0 + p.OnWall = true } else if typeY == "obstacle" { // Obstacle - immer töten p.Y = nextY @@ -774,6 +790,7 @@ func (r *Room) Broadcast() { TimeLeft: r.Countdown, WorldChunks: r.ActiveChunks, HostID: r.HostID, + TeamName: r.TeamName, ScrollX: r.GlobalScrollX, CollectedCoins: r.CollectedCoins, CollectedPowerups: r.CollectedPowerups, diff --git a/pkg/server/scoring.go b/pkg/server/scoring.go index 95fc9d4..5d87ab7 100644 --- a/pkg/server/scoring.go +++ b/pkg/server/scoring.go @@ -60,7 +60,8 @@ func (r *Room) CheckCoinCollision(p *ServerPlayer) { if game.CheckRectCollision(playerHitbox, coinHitbox) { // Coin einsammeln! r.CollectedCoins[coinKey] = true - p.Score += 200 + p.BonusScore += 200 + p.Score = p.DistanceScore + p.BonusScore log.Printf("💰 %s hat Coin eingesammelt! Score: %d", p.Name, p.Score) } } @@ -143,18 +144,28 @@ func (r *Room) UpdateDistanceScore() { return } - // Jeder Spieler bekommt Punkte basierend auf seiner eigenen Distanz - // Punkte = (X-Position / TileSize) = Distanz in Tiles + // Zähle lebende Spieler + aliveCount := 0 for _, p := range r.Players { if p.IsAlive && !p.IsSpectator { - // Berechne Score basierend auf X-Position - // 1 Punkt pro Tile (64px) - newScore := int(p.X / 64.0) + aliveCount++ + } + } - // Nur updaten wenn höher als aktueller Score - if newScore > p.Score { - p.Score = newScore - } + if aliveCount == 0 { + return + } + + // Pro lebendem Spieler werden Punkte hinzugefügt + // Dies akkumuliert die Punkte: mehr Spieler = schnellere Punktesammlung + // Jeder Tick (bei 60 FPS) fügt aliveCount Punkte hinzu + pointsToAdd := aliveCount + + // Jeder lebende Spieler bekommt die gleichen Punkte + for _, p := range r.Players { + if p.IsAlive && !p.IsSpectator { + p.DistanceScore += pointsToAdd + p.Score = p.DistanceScore + p.BonusScore } } }