Private
Public Access
1
0
Files
EscapeFromTeacher/cmd/client/connection_wasm.go
Sebastian Unterschütz ce092c8366
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 8m13s
fix game
2026-03-21 22:18:48 +01:00

418 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//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() {
// Generation erhöhen → alle noch laufenden Handler der alten Verbindung werden ignoriert
g.connGeneration++
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,
}
// Generation zum Zeitpunkt der Verbindung festhalten.
// Wenn disconnectFromServer() aufgerufen wird, erhöht sich g.connGeneration
// und alle Handler dieser alten Verbindung geben sofort nil zurück.
myGen := g.connGeneration
// OnOpen Handler
ws.Call("addEventListener", "open", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if g.connGeneration != myGen {
return nil // veraltete Verbindung
}
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 g.connGeneration != myGen {
return nil // veraltete Verbindung Nachrichten ignorieren
}
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{} {
if g.connGeneration != myGen {
return nil // veraltete Verbindung
}
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{} {
if g.connGeneration != myGen {
return nil // veraltete Verbindung kein g.connected überschreiben
}
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)
}