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 = '
Team-Name (nur Host):
+ +Aktuell: Nicht gesetzt
+