All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 8m13s
418 lines
10 KiB
Go
418 lines
10 KiB
Go
//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)
|
||
}
|