//go:build wasm // +build wasm package main import ( "encoding/json" "log" "syscall/js" "time" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" ) // WebSocketMessage ist das Format für WebSocket-Nachrichten type WebSocketMessage struct { Type string `json:"type"` Payload interface{} `json:"payload"` } // wsConn verwaltet die WebSocket-Verbindung im Browser type wsConn struct { ws js.Value messagesChan chan []byte connected bool } // disconnectFromServer trennt die bestehende WebSocket-Verbindung sauber func (g *Game) disconnectFromServer() { if g.wsConn != nil { g.wsConn.ws.Call("close") g.wsConn = nil } g.connected = false } // connectToServer verbindet sich über WebSocket mit dem Gateway func (g *Game) connectToServer() { // Automatisch die richtige WebSocket-URL basierend auf der aktuellen Domain protocol := "ws:" if js.Global().Get("location").Get("protocol").String() == "https:" { protocol = "wss:" } host := js.Global().Get("location").Get("host").String() serverURL := protocol + "//" + host + "/ws" log.Printf("🔌 Verbinde zu WebSocket-Gateway: %s", serverURL) ws := js.Global().Get("WebSocket").New(serverURL) conn := &wsConn{ ws: ws, messagesChan: make(chan []byte, 100), connected: false, } // OnOpen Handler ws.Call("addEventListener", "open", js.FuncOf(func(this js.Value, args []js.Value) interface{} { log.Println("✅ WebSocket verbunden!") conn.connected = true g.connected = true return nil })) // OnMessage Handler ws.Call("addEventListener", "message", js.FuncOf(func(this js.Value, args []js.Value) interface{} { if len(args) == 0 { return nil } data := args[0].Get("data").String() var msg WebSocketMessage if err := json.Unmarshal([]byte(data), &msg); err != nil { log.Printf("❌ Fehler beim Parsen der Nachricht: %v", err) return nil } switch msg.Type { case "game_update": // GameState Update payloadBytes, _ := json.Marshal(msg.Payload) var state game.GameState if err := json.Unmarshal(payloadBytes, &state); err == nil { // Out-of-Order-Erkennung: Ignoriere alte Updates if state.Sequence > 0 && state.Sequence <= g.lastRecvSeq { // Alte Nachricht - ignorieren g.outOfOrderCount++ return nil } g.lastRecvSeq = state.Sequence g.totalUpdates++ g.lastUpdateTime = time.Now() // Aktualisiere CurrentSpeed für Client-Prediction g.predictionMutex.Lock() g.currentSpeed = state.CurrentSpeed g.predictionMutex.Unlock() // Server Reconciliation für lokalen Spieler (VOR dem Lock) for _, p := range state.Players { if p.Name == g.playerName { g.ReconcileWithServer(p) break } } g.stateMutex.Lock() oldPlayerCount := len(g.gameState.Players) g.gameState = state newPlayerCount := len(g.gameState.Players) g.stateMutex.Unlock() // Lobby-UI aktualisieren wenn sich Spieleranzahl geändert hat if oldPlayerCount != newPlayerCount { g.sendLobbyUpdateToJS() } } case "leaderboard_response": // Leaderboard Response payloadBytes, _ := json.Marshal(msg.Payload) var resp game.LeaderboardResponse if err := json.Unmarshal(payloadBytes, &resp); err == nil { g.leaderboardMutex.Lock() g.leaderboard = resp.Entries g.leaderboardMutex.Unlock() log.Printf("📊 Leaderboard empfangen: %d Einträge", len(resp.Entries)) // 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 })) // OnError Handler ws.Call("addEventListener", "error", js.FuncOf(func(this js.Value, args []js.Value) interface{} { log.Println("❌ WebSocket Fehler!") conn.connected = false g.connected = false return nil })) // OnClose Handler ws.Call("addEventListener", "close", js.FuncOf(func(this js.Value, args []js.Value) interface{} { log.Println("🔌 WebSocket geschlossen") conn.connected = false g.connected = false return nil })) // Warte bis Verbindung hergestellt ist for i := 0; i < 50; i++ { if conn.connected { break } time.Sleep(100 * time.Millisecond) } if !conn.connected { log.Println("❌ WebSocket-Verbindung Timeout") return } // WebSocket-Wrapper speichern g.wsConn = conn // JOIN senden joinMsg := WebSocketMessage{ Type: "join", Payload: game.JoinRequest{ Name: g.playerName, RoomID: g.roomID, GameMode: g.gameMode, IsHost: g.isHost, TeamName: g.teamName, PlayerCode: g.playerCode, }, } g.sendWebSocketMessage(joinMsg) log.Printf("➡️ JOIN gesendet über WebSocket: Name=%s, RoomID=%s, PlayerCode=%s", g.playerName, g.roomID, g.playerCode) } // sendWebSocketMessage sendet eine Nachricht über WebSocket func (g *Game) sendWebSocketMessage(msg WebSocketMessage) { if g.wsConn == nil || !g.wsConn.connected { log.Println("⚠️ WebSocket nicht verbunden") return } data, err := json.Marshal(msg) if err != nil { log.Printf("❌ Fehler beim Marshallen: %v", err) return } g.wsConn.ws.Call("send", string(data)) } // sendInput sendet einen Input über WebSocket func (g *Game) sendInput(input game.ClientInput) { msg := WebSocketMessage{ Type: "input", Payload: input, } g.sendWebSocketMessage(msg) } // startGame sendet den Start-Befehl über WebSocket func (g *Game) startGame() { // PlayerID ist der playerName (Server verwendet req.Name als PlayerID) myID := g.playerName msg := WebSocketMessage{ Type: "start", Payload: game.StartRequest{ RoomID: g.roomID, PlayerID: myID, }, } g.sendWebSocketMessage(msg) log.Printf("▶️ START gesendet über WebSocket: RoomID=%s, PlayerID=%s", g.roomID, myID) } // connectForLeaderboard verbindet für Leaderboard-Abfrage func (g *Game) connectForLeaderboard() { if g.wsConn != nil && g.wsConn.connected { // Bereits verbunden g.requestLeaderboard() return } // Temporäre Daten für Leaderboard-Verbindung if g.playerName == "" { g.playerName = "LeaderboardViewer" } if g.roomID == "" { g.roomID = "leaderboard_only" } if g.gameMode == "" { g.gameMode = "solo" } // Neue Verbindung aufbauen g.connectToServer() // Kurz warten und dann Leaderboard anfragen time.Sleep(500 * time.Millisecond) g.requestLeaderboard() } // requestLeaderboard fordert das Leaderboard an func (g *Game) requestLeaderboard() { // Verbindung aufbauen falls nicht vorhanden if g.wsConn == nil || !g.wsConn.connected { log.Println("📡 Keine Verbindung für Leaderboard, stelle Verbindung her...") go g.connectForLeaderboard() return } mode := "solo" if g.gameMode == "coop" { mode = "coop" } msg := WebSocketMessage{ Type: "leaderboard_request", Payload: game.LeaderboardRequest{ Mode: mode, }, } g.sendWebSocketMessage(msg) log.Printf("🏆 Leaderboard-Request gesendet: Mode=%s", mode) } // submitScore sendet den Score ans Leaderboard func (g *Game) submitScore() { if g.scoreSubmitted { return } g.stateMutex.Lock() score := 0 for _, p := range g.gameState.Players { if p.Name == g.playerName { score = p.Score break } } g.stateMutex.Unlock() if score == 0 { log.Println("⚠️ Score ist 0, überspringe Submission") return } name := g.playerName if g.gameMode == "coop" && g.teamName != "" { name = g.teamName } // Verwende Team-Name für Coop-Mode, sonst Player-Name displayName := name teamName := "" playerCodeToUse := g.playerCode if g.gameMode == "coop" { g.stateMutex.Lock() teamName = g.gameState.TeamName hostPlayerCode := g.gameState.HostPlayerCode g.stateMutex.Unlock() if teamName != "" { displayName = teamName } // In Coop: Verwende Host's PlayerCode für Score if hostPlayerCode != "" { playerCodeToUse = hostPlayerCode log.Printf("🔑 Coop-Mode: Verwende Host PlayerCode für Score-Submission") } } msg := WebSocketMessage{ Type: "score_submit", Payload: game.ScoreSubmission{ PlayerName: displayName, PlayerCode: playerCodeToUse, 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 (PlayerCode: %s, TeamName: %s)", displayName, score, playerCodeToUse, teamName) } // Dummy-Funktionen für Kompatibilität mit anderen Teilen des Codes func (g *Game) sendJoinRequest() { // Wird in connectToServer aufgerufen } 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) }