Add WebAssembly support for assets and chunks, implement gameover screen rendering, and enhance server gameplay logic with dynamic speeds, team naming, and score components.
@@ -30,7 +30,7 @@ import (
|
|||||||
// --- CONFIG ---
|
// --- CONFIG ---
|
||||||
const (
|
const (
|
||||||
RawDir = "./assets_raw"
|
RawDir = "./assets_raw"
|
||||||
OutFile = "./cmd/client/assets/assets.json"
|
OutFile = "cmd/client/web/assets/assets.json"
|
||||||
|
|
||||||
WidthList = 280 // Etwas breiter für die Bilder
|
WidthList = 280 // Etwas breiter für die Bilder
|
||||||
WidthInspect = 300
|
WidthInspect = 300
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
func (g *Game) loadAssets() {
|
func (g *Game) loadAssets() {
|
||||||
// Pfad anpassen: Wir suchen im relativen Pfad
|
// Pfad anpassen: Wir suchen im relativen Pfad
|
||||||
baseDir := "./cmd/client/assets"
|
baseDir := "./cmd/client/web/assets"
|
||||||
manifestPath := filepath.Join(baseDir, "assets.json")
|
manifestPath := filepath.Join(baseDir, "assets.json")
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(manifestPath)
|
data, err := ioutil.ReadFile(manifestPath)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// loadChunks lädt alle Chunks aus dem Verzeichnis (Native Desktop)
|
// loadChunks lädt alle Chunks aus dem Verzeichnis (Native Desktop)
|
||||||
func (g *Game) loadChunks() {
|
func (g *Game) loadChunks() {
|
||||||
baseDir := "cmd/client/assets"
|
baseDir := "cmd/client/web/assets"
|
||||||
chunkDir := filepath.Join(baseDir, "chunks")
|
chunkDir := filepath.Join(baseDir, "chunks")
|
||||||
|
|
||||||
err := g.world.LoadChunkLibrary(chunkDir)
|
err := g.world.LoadChunkLibrary(chunkDir)
|
||||||
|
|||||||
@@ -252,17 +252,32 @@ func (g *Game) submitScore() {
|
|||||||
name = g.teamName
|
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{
|
msg := WebSocketMessage{
|
||||||
Type: "score_submit",
|
Type: "score_submit",
|
||||||
Payload: game.ScoreSubmission{
|
Payload: game.ScoreSubmission{
|
||||||
|
PlayerName: displayName,
|
||||||
PlayerCode: g.playerCode,
|
PlayerCode: g.playerCode,
|
||||||
Name: name,
|
Name: displayName, // Für Kompatibilität
|
||||||
Score: score,
|
Score: score,
|
||||||
Mode: g.gameMode,
|
Mode: g.gameMode,
|
||||||
|
TeamName: teamName, // Team-Name für Coop
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
g.sendWebSocketMessage(msg)
|
g.sendWebSocketMessage(msg)
|
||||||
|
|
||||||
g.scoreSubmitted = true
|
g.scoreSubmitted = true
|
||||||
log.Printf("📊 Score submitted: %s = %d", name, score)
|
log.Printf("📊 Score submitted: %s = %d (TeamName: %s)", displayName, score, teamName)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,19 @@ func (g *Game) connectToServer() {
|
|||||||
// An JavaScript senden
|
// An JavaScript senden
|
||||||
g.sendLeaderboardToJS()
|
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
|
return nil
|
||||||
@@ -259,19 +272,34 @@ func (g *Game) submitScore() {
|
|||||||
name = g.teamName
|
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{
|
msg := WebSocketMessage{
|
||||||
Type: "score_submit",
|
Type: "score_submit",
|
||||||
Payload: game.ScoreSubmission{
|
Payload: game.ScoreSubmission{
|
||||||
|
PlayerName: displayName,
|
||||||
PlayerCode: g.playerCode,
|
PlayerCode: g.playerCode,
|
||||||
Name: name,
|
Name: displayName, // Für Kompatibilität
|
||||||
Score: score,
|
Score: score,
|
||||||
Mode: g.gameMode,
|
Mode: g.gameMode,
|
||||||
|
TeamName: teamName, // Team-Name für Coop
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
g.sendWebSocketMessage(msg)
|
g.sendWebSocketMessage(msg)
|
||||||
|
|
||||||
g.scoreSubmitted = true
|
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
|
// Dummy-Funktionen für Kompatibilität mit anderen Teilen des Codes
|
||||||
@@ -280,9 +308,41 @@ func (g *Game) sendJoinRequest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) sendStartRequest() {
|
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()
|
g.startGame()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) publishInput(input game.ClientInput) {
|
func (g *Game) publishInput(input game.ClientInput) {
|
||||||
g.sendInput(input)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"sort"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game"
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
"github.com/hajimehoshi/ebiten/v2/text"
|
"github.com/hajimehoshi/ebiten/v2/text"
|
||||||
@@ -222,12 +219,11 @@ func (g *Game) DrawGame(screen *ebiten.Image) {
|
|||||||
|
|
||||||
// In WASM: HTML Game Over Screen anzeigen
|
// In WASM: HTML Game Over Screen anzeigen
|
||||||
if !g.scoreSubmitted {
|
if !g.scoreSubmitted {
|
||||||
g.scoreSubmitted = true
|
g.submitScore() // submitScore() setzt g.scoreSubmitted intern
|
||||||
g.submitScore()
|
|
||||||
g.sendGameOverToJS(myScore) // Zeigt HTML Game Over Screen
|
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
|
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})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,7 +3,172 @@
|
|||||||
|
|
||||||
package main
|
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)
|
// sendGameOverToJS ist ein Stub für Native (kein HTML)
|
||||||
func (g *Game) sendGameOverToJS(score int) {
|
func (g *Game) sendGameOverToJS(score int) {
|
||||||
// Native hat kein HTML-Overlay, nichts zu tun
|
// 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})
|
||||||
|
}
|
||||||
|
|||||||
16
cmd/client/gameover_wasm.go
Normal file
@@ -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})
|
||||||
|
}
|
||||||
@@ -28,11 +28,11 @@ func (g *Game) setupJavaScriptBridge() {
|
|||||||
g.savePlayerName(playerName)
|
g.savePlayerName(playerName)
|
||||||
|
|
||||||
if mode == "solo" {
|
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.roomID = fmt.Sprintf("solo_%d", time.Now().UnixNano())
|
||||||
g.isHost = true
|
g.isHost = true
|
||||||
g.appState = StateGame
|
g.appState = StateLobby // Warte auf Server Auto-Start
|
||||||
log.Printf("🎮 Solo-Spiel gestartet: %s", playerName)
|
log.Printf("🎮 Solo-Spiel gestartet: %s (warte auf Server)", playerName)
|
||||||
} else if mode == "coop" && len(args) >= 5 {
|
} else if mode == "coop" && len(args) >= 5 {
|
||||||
// Co-op Mode - in die Lobby
|
// Co-op Mode - in die Lobby
|
||||||
roomID := args[2].String()
|
roomID := args[2].String()
|
||||||
@@ -87,12 +87,24 @@ func (g *Game) setupJavaScriptBridge() {
|
|||||||
return nil
|
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
|
// Im globalen Scope registrieren
|
||||||
js.Global().Set("startGame", startGameFunc)
|
js.Global().Set("startGame", startGameFunc)
|
||||||
js.Global().Set("requestLeaderboard", requestLeaderboardFunc)
|
js.Global().Set("requestLeaderboard", requestLeaderboardFunc)
|
||||||
js.Global().Set("setMusicVolume", setMusicVolumeFunc)
|
js.Global().Set("setMusicVolume", setMusicVolumeFunc)
|
||||||
js.Global().Set("setSFXVolume", setSFXVolumeFunc)
|
js.Global().Set("setSFXVolume", setSFXVolumeFunc)
|
||||||
js.Global().Set("startGameFromLobby_WASM", startGameFromLobbyFunc)
|
js.Global().Set("startGameFromLobby_WASM", startGameFromLobbyFunc)
|
||||||
|
js.Global().Set("setTeamName_WASM", setTeamNameFunc)
|
||||||
|
|
||||||
log.Println("✅ JavaScript Bridge registriert")
|
log.Println("✅ JavaScript Bridge registriert")
|
||||||
}
|
}
|
||||||
@@ -131,6 +143,7 @@ func (g *Game) sendLobbyPlayersToJS() {
|
|||||||
g.stateMutex.Lock()
|
g.stateMutex.Lock()
|
||||||
players := make([]interface{}, 0, len(g.gameState.Players))
|
players := make([]interface{}, 0, len(g.gameState.Players))
|
||||||
hostID := g.gameState.HostID
|
hostID := g.gameState.HostID
|
||||||
|
teamName := g.gameState.TeamName
|
||||||
|
|
||||||
for id, p := range g.gameState.Players {
|
for id, p := range g.gameState.Players {
|
||||||
name := p.Name
|
name := p.Name
|
||||||
@@ -151,4 +164,12 @@ func (g *Game) sendLobbyPlayersToJS() {
|
|||||||
updateFunc.Invoke(jsPlayers)
|
updateFunc.Invoke(jsPlayers)
|
||||||
log.Printf("👥 Lobby-Spieler an JavaScript gesendet: %d Spieler", len(players))
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
```
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../assets
|
|
||||||
|
Before Width: | Height: | Size: 142 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 4.1 MiB After Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 1.5 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 4.1 MiB After Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 1.4 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 366 KiB After Width: | Height: | Size: 366 KiB |
|
Before Width: | Height: | Size: 222 B After Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 222 B After Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 653 KiB After Width: | Height: | Size: 653 KiB |
|
Before Width: | Height: | Size: 3.0 MiB After Width: | Height: | Size: 3.0 MiB |
|
Before Width: | Height: | Size: 559 KiB After Width: | Height: | Size: 559 KiB |
|
Before Width: | Height: | Size: 545 KiB After Width: | Height: | Size: 545 KiB |
|
Before Width: | Height: | Size: 696 KiB After Width: | Height: | Size: 696 KiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 4.2 MiB After Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 222 B After Width: | Height: | Size: 222 B |
|
Before Width: | Height: | Size: 384 B After Width: | Height: | Size: 384 B |
|
Before Width: | Height: | Size: 2.9 MiB After Width: | Height: | Size: 2.9 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
BIN
cmd/client/web/background.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
@@ -2,6 +2,130 @@
|
|||||||
let wasmReady = false;
|
let wasmReady = false;
|
||||||
let gameStarted = false;
|
let gameStarted = false;
|
||||||
let audioMuted = 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)
|
// WebSocket for Leaderboard (direct JS connection)
|
||||||
let leaderboardWS = null;
|
let leaderboardWS = null;
|
||||||
@@ -89,8 +213,11 @@ async function initWASM() {
|
|||||||
go.run(result.instance);
|
go.run(result.instance);
|
||||||
wasmReady = true;
|
wasmReady = true;
|
||||||
|
|
||||||
// Hide loading screen
|
// Switch to menu state
|
||||||
document.getElementById('loading').style.display = 'none';
|
setUIState(UIState.MENU);
|
||||||
|
|
||||||
|
// Enable all start buttons
|
||||||
|
enableStartButtons();
|
||||||
|
|
||||||
console.log('✅ WASM loaded successfully');
|
console.log('✅ WASM loaded successfully');
|
||||||
|
|
||||||
@@ -104,53 +231,32 @@ async function initWASM() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu Navigation
|
// Enable start buttons after WASM is ready
|
||||||
function showMainMenu() {
|
function enableStartButtons() {
|
||||||
hideAllScreens();
|
const buttons = ['startBtn', 'coopBtn', 'createRoomBtn', 'joinRoomBtn'];
|
||||||
document.getElementById('menu').classList.remove('hidden');
|
buttons.forEach(btnId => {
|
||||||
}
|
const btn = document.getElementById(btnId);
|
||||||
|
if (btn) {
|
||||||
function showCoopMenu() {
|
btn.disabled = false;
|
||||||
hideAllScreens();
|
btn.style.opacity = '1';
|
||||||
document.getElementById('coopMenu').classList.remove('hidden');
|
btn.style.cursor = 'pointer';
|
||||||
}
|
}
|
||||||
|
|
||||||
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');
|
|
||||||
});
|
});
|
||||||
|
console.log('✅ Start-Buttons aktiviert (Solo + Coop)');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideMenu() {
|
// Menu Navigation
|
||||||
document.getElementById('menu').style.display = 'none';
|
// Legacy function wrappers - use setUIState instead
|
||||||
// Canvas sichtbar machen für Gameplay
|
function showMainMenu() { setUIState(UIState.MENU); }
|
||||||
const canvas = document.querySelector('canvas');
|
function showCoopMenu() { setUIState(UIState.COOP_MENU); }
|
||||||
if (canvas) {
|
function showSettings() { setUIState(UIState.SETTINGS); }
|
||||||
canvas.classList.add('game-active');
|
function showLeaderboard() { setUIState(UIState.LEADERBOARD); loadLeaderboard(); }
|
||||||
}
|
function showMyCodes() { setUIState(UIState.MY_CODES); loadMyCodes(); }
|
||||||
}
|
function showImpressum() { setUIState(UIState.IMPRESSUM); }
|
||||||
|
function showDatenschutz() { setUIState(UIState.DATENSCHUTZ); }
|
||||||
function showMenu() {
|
function hideAllScreens() { /* Handled by setUIState */ }
|
||||||
document.getElementById('menu').style.display = 'flex';
|
function hideMenu() { /* Handled by setUIState */ }
|
||||||
document.getElementById('menu').classList.remove('hidden');
|
function showMenu() { setUIState(UIState.MENU); }
|
||||||
showMainMenu();
|
|
||||||
// Canvas verstecken im Menü
|
|
||||||
const canvas = document.querySelector('canvas');
|
|
||||||
if (canvas) {
|
|
||||||
canvas.classList.remove('game-active');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Game Functions
|
// Game Functions
|
||||||
function startSoloGame() {
|
function startSoloGame() {
|
||||||
@@ -166,23 +272,17 @@ function startSoloGame() {
|
|||||||
localStorage.setItem('escape_game_mode', 'solo');
|
localStorage.setItem('escape_game_mode', 'solo');
|
||||||
localStorage.setItem('escape_room_id', '');
|
localStorage.setItem('escape_room_id', '');
|
||||||
|
|
||||||
// Hide ALL screens including main menu
|
|
||||||
hideAllScreens();
|
|
||||||
document.getElementById('menu').style.display = 'none';
|
|
||||||
gameStarted = true;
|
gameStarted = true;
|
||||||
|
|
||||||
// Canvas sichtbar machen
|
// Don't switch UI state yet - wait for WASM callback onGameStarted()
|
||||||
const canvas = document.querySelector('canvas');
|
// The server will auto-start solo games after 2 seconds
|
||||||
if (canvas) {
|
|
||||||
canvas.classList.add('game-active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger WASM game start
|
// Trigger WASM game start
|
||||||
if (window.startGame) {
|
if (window.startGame) {
|
||||||
window.startGame('solo', playerName, '');
|
window.startGame('solo', playerName, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🎮 Solo game started:', playerName);
|
console.log('🎮 Solo game starting - waiting for server auto-start...');
|
||||||
}
|
}
|
||||||
|
|
||||||
function createRoom() {
|
function createRoom() {
|
||||||
@@ -202,12 +302,8 @@ function createRoom() {
|
|||||||
localStorage.setItem('escape_team_name', teamName);
|
localStorage.setItem('escape_team_name', teamName);
|
||||||
localStorage.setItem('escape_is_host', 'true');
|
localStorage.setItem('escape_is_host', 'true');
|
||||||
|
|
||||||
// Verstecke ALLE Screens inkl. Hauptmenü
|
// Show Lobby
|
||||||
hideAllScreens();
|
setUIState(UIState.LOBBY);
|
||||||
document.getElementById('menu').style.display = 'none';
|
|
||||||
|
|
||||||
// Zeige HTML Lobby Screen
|
|
||||||
document.getElementById('lobbyScreen').classList.remove('hidden');
|
|
||||||
document.getElementById('lobbyRoomCode').textContent = roomID;
|
document.getElementById('lobbyRoomCode').textContent = roomID;
|
||||||
document.getElementById('lobbyHostControls').classList.remove('hidden');
|
document.getElementById('lobbyHostControls').classList.remove('hidden');
|
||||||
document.getElementById('lobbyStatus').textContent = 'Du bist Host - starte wenn bereit!';
|
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_team_name', teamName);
|
||||||
localStorage.setItem('escape_is_host', 'false');
|
localStorage.setItem('escape_is_host', 'false');
|
||||||
|
|
||||||
// Verstecke ALLE Screens inkl. Hauptmenü
|
// Show Lobby
|
||||||
hideAllScreens();
|
setUIState(UIState.LOBBY);
|
||||||
document.getElementById('menu').style.display = 'none';
|
|
||||||
|
|
||||||
// Zeige HTML Lobby Screen
|
|
||||||
document.getElementById('lobbyScreen').classList.remove('hidden');
|
|
||||||
document.getElementById('lobbyRoomCode').textContent = roomID;
|
document.getElementById('lobbyRoomCode').textContent = roomID;
|
||||||
document.getElementById('lobbyHostControls').classList.add('hidden');
|
document.getElementById('lobbyHostControls').classList.add('hidden');
|
||||||
document.getElementById('lobbyStatus').textContent = 'Warte auf Host...';
|
document.getElementById('lobbyStatus').textContent = 'Warte auf Host...';
|
||||||
@@ -291,6 +383,42 @@ function updateLobbyPlayers(players) {
|
|||||||
console.log('👥 Lobby players updated:', players.length);
|
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() {
|
function loadLeaderboard() {
|
||||||
const list = document.getElementById('leaderboardList');
|
const list = document.getElementById('leaderboardList');
|
||||||
list.innerHTML = '<div style="text-align:center; padding:20px;">Lädt Leaderboard...</div>';
|
list.innerHTML = '<div style="text-align:center; padding:20px;">Lädt Leaderboard...</div>';
|
||||||
@@ -308,6 +436,9 @@ function loadLeaderboard() {
|
|||||||
|
|
||||||
// Called by WASM to update leaderboard
|
// Called by WASM to update leaderboard
|
||||||
function updateLeaderboard(entries) {
|
function updateLeaderboard(entries) {
|
||||||
|
// Store full leaderboard data globally
|
||||||
|
currentLeaderboard = entries || [];
|
||||||
|
|
||||||
// Update ALL leaderboard displays
|
// Update ALL leaderboard displays
|
||||||
const list = document.getElementById('leaderboardList');
|
const list = document.getElementById('leaderboardList');
|
||||||
const startList = document.getElementById('startLeaderboardList');
|
const startList = document.getElementById('startLeaderboardList');
|
||||||
@@ -423,8 +554,7 @@ document.addEventListener('keydown', (e) => {
|
|||||||
|
|
||||||
// Show Game Over Screen (called by WASM)
|
// Show Game Over Screen (called by WASM)
|
||||||
function showGameOver(score) {
|
function showGameOver(score) {
|
||||||
hideAllScreens();
|
setUIState(UIState.GAME_OVER);
|
||||||
document.getElementById('gameOverScreen').classList.remove('hidden');
|
|
||||||
document.getElementById('finalScore').textContent = score;
|
document.getElementById('finalScore').textContent = score;
|
||||||
|
|
||||||
// Update local highscore
|
// Update local highscore
|
||||||
@@ -436,29 +566,137 @@ function showGameOver(score) {
|
|||||||
// Request leaderboard via direct WebSocket
|
// Request leaderboard via direct WebSocket
|
||||||
requestLeaderboardDirect();
|
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
|
// Called by WASM when game actually starts
|
||||||
function onGameStarted() {
|
function onGameStarted() {
|
||||||
console.log('🎮 Game Started - Making canvas visible');
|
console.log('🎮 Game Started - Making canvas visible');
|
||||||
hideAllScreens();
|
|
||||||
document.getElementById('menu').style.display = 'none';
|
|
||||||
gameStarted = true;
|
gameStarted = true;
|
||||||
|
setUIState(UIState.PLAYING);
|
||||||
|
}
|
||||||
|
|
||||||
// Canvas sichtbar machen
|
// ===== MY CODES MANAGEMENT =====
|
||||||
const canvas = document.querySelector('canvas');
|
|
||||||
if (canvas) {
|
// Save highscore code to localStorage
|
||||||
canvas.classList.add('game-active');
|
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 = '<div style="color: #888; text-align: center; padding: 20px;">Noch keine Highscores erreicht!</div>';
|
||||||
|
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 = '<div style="color: #888; text-align: center; padding: 20px;">Lade Positionen...</div>';
|
||||||
|
|
||||||
|
// 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 += `
|
||||||
|
<div style="background: rgba(0,0,0,0.4); border: 2px solid #fc0; padding: 12px; margin: 8px 0; border-radius: 4px;">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||||
|
<div>
|
||||||
|
<span style="color: #fc0; font-size: 20px; font-weight: bold;">${code.score} Punkte</span>
|
||||||
|
<span style="color: ${positionColor}; font-size: 14px; margin-left: 10px;">${positionText}</span>
|
||||||
|
</div>
|
||||||
|
<button onclick="deleteHighscoreCode(${index})" style="background: #ff4444; border: none; color: white; padding: 5px 10px; font-size: 10px; cursor: pointer; border-radius: 3px;">LÖSCHEN</button>
|
||||||
|
</div>
|
||||||
|
<div style="font-family: sans-serif; font-size: 12px; color: #ccc;">
|
||||||
|
<div><strong>Name:</strong> ${code.player_name}</div>
|
||||||
|
<div><strong>Code:</strong> <span style="color: #fc0; font-family: monospace;">${code.proof}</span></div>
|
||||||
|
<div><strong>Datum:</strong> ${code.date}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
// Export functions for WASM to call
|
||||||
window.showMenu = showMenu;
|
window.showMenu = showMenu;
|
||||||
window.hideMenu = hideMenu;
|
window.hideMenu = hideMenu;
|
||||||
window.updateLeaderboard = updateLeaderboard;
|
window.updateLeaderboard = updateLeaderboard;
|
||||||
window.showGameOver = showGameOver;
|
window.showGameOver = showGameOver;
|
||||||
window.onGameStarted = onGameStarted;
|
window.onGameStarted = onGameStarted;
|
||||||
|
window.saveHighscoreCode = saveHighscoreCode;
|
||||||
|
|
||||||
// Initialize on load
|
// Initialize on load
|
||||||
initWASM();
|
initWASM();
|
||||||
|
|||||||
@@ -27,8 +27,8 @@
|
|||||||
|
|
||||||
<input type="text" id="playerName" placeholder="NAME (4 ZEICHEN)" maxlength="15" style="text-transform:uppercase;">
|
<input type="text" id="playerName" placeholder="NAME (4 ZEICHEN)" maxlength="15" style="text-transform:uppercase;">
|
||||||
|
|
||||||
<button id="startBtn" onclick="startSoloGame()">SOLO STARTEN</button>
|
<button id="startBtn" onclick="startSoloGame()" disabled style="opacity: 0.5; cursor: not-allowed;">SOLO STARTEN</button>
|
||||||
<button id="coopBtn" onclick="showCoopMenu()">CO-OP SPIELEN</button>
|
<button id="coopBtn" onclick="showCoopMenu()" disabled style="opacity: 0.5; cursor: not-allowed;">CO-OP SPIELEN</button>
|
||||||
|
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<div class="info-title">SCHUL-NEWS</div>
|
<div class="info-title">SCHUL-NEWS</div>
|
||||||
@@ -49,8 +49,14 @@
|
|||||||
|
|
||||||
<div class="legal-bar">
|
<div class="legal-bar">
|
||||||
<button class="legal-btn" onclick="showLeaderboard()">🏆 TOP 10</button>
|
<button class="legal-btn" onclick="showLeaderboard()">🏆 TOP 10</button>
|
||||||
|
<button class="legal-btn" onclick="showMyCodes()">🔑 MEINE CODES</button>
|
||||||
<button class="legal-btn" onclick="showSettings()">⚙️ EINSTELLUNGEN</button>
|
<button class="legal-btn" onclick="showSettings()">⚙️ EINSTELLUNGEN</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="legal-bar" style="margin-top: 10px;">
|
||||||
|
<button class="legal-btn" onclick="showImpressum()">📄 IMPRESSUM</button>
|
||||||
|
<button class="legal-btn" onclick="showDatenschutz()">🔒 DATENSCHUTZ</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="start-right">
|
<div class="start-right">
|
||||||
@@ -67,13 +73,13 @@
|
|||||||
<div class="center-box">
|
<div class="center-box">
|
||||||
<h1>CO-OP MODUS</h1>
|
<h1>CO-OP MODUS</h1>
|
||||||
|
|
||||||
<button class="big-btn" onclick="createRoom()">RAUM ERSTELLEN</button>
|
<button id="createRoomBtn" class="big-btn" onclick="createRoom()" disabled style="opacity: 0.5; cursor: not-allowed;">RAUM ERSTELLEN</button>
|
||||||
|
|
||||||
<div style="margin: 20px 0;">- ODER -</div>
|
<div style="margin: 20px 0;">- ODER -</div>
|
||||||
|
|
||||||
<input type="text" id="joinRoomCode" placeholder="RAUM-CODE" maxlength="6" style="text-transform:uppercase;">
|
<input type="text" id="joinRoomCode" placeholder="RAUM-CODE" maxlength="6" style="text-transform:uppercase;">
|
||||||
<input type="text" id="teamNameJoin" placeholder="TEAM-NAME" maxlength="15">
|
<input type="text" id="teamNameJoin" placeholder="TEAM-NAME" maxlength="15">
|
||||||
<button onclick="joinRoom()">RAUM BEITRETEN</button>
|
<button id="joinRoomBtn" onclick="joinRoom()" disabled style="opacity: 0.5; cursor: not-allowed;">RAUM BEITRETEN</button>
|
||||||
|
|
||||||
<button class="back-btn" onclick="showMainMenu()">← ZURÜCK</button>
|
<button class="back-btn" onclick="showMainMenu()">← ZURÜCK</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -115,6 +121,112 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- MY CODES MENU -->
|
||||||
|
<div id="myCodesMenu" class="overlay-screen hidden">
|
||||||
|
<div class="center-box">
|
||||||
|
<h1>🔑 MEINE HIGHSCORE-CODES</h1>
|
||||||
|
|
||||||
|
<div id="myCodesList" class="leaderboard-box" style="max-height: 500px;">
|
||||||
|
Keine Codes gespeichert.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="back-btn" onclick="showMainMenu()">← ZURÜCK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- IMPRESSUM MENU -->
|
||||||
|
<div id="impressumMenu" class="overlay-screen hidden">
|
||||||
|
<div class="center-box" style="max-width: 800px;">
|
||||||
|
<h1>📄 IMPRESSUM & CREDITS</h1>
|
||||||
|
|
||||||
|
<div class="leaderboard-box" style="max-height: 500px; overflow-y: auto; text-align: left; font-family: sans-serif; font-size: 13px; line-height: 1.6; background: #333; color: #fff;">
|
||||||
|
<h3 style="color: #fc0; margin-top: 0;">Projektleitung & Code:</h3>
|
||||||
|
<p>Sebastian Unterschütz<br>
|
||||||
|
Göltzschtalblick 16<br>
|
||||||
|
08236 Ellefeld<br>
|
||||||
|
<strong>Kontakt:</strong> sebastian@unterschuetz.de</p>
|
||||||
|
|
||||||
|
<h3 style="color: #fc0; margin-top: 20px;">🎵 Musik & Sound Design:</h3>
|
||||||
|
<p>Max E.</p>
|
||||||
|
|
||||||
|
<h3 style="color: #fc0; margin-top: 20px;">💻 Quellcode:</h3>
|
||||||
|
<p><a href="https://git.zb-server.de/ZB-Server/it232Abschied" target="_blank" style="color: #fc0;">git.zb-server.de/ZB-Server/it232Abschied</a></p>
|
||||||
|
|
||||||
|
<h3 style="color: #fc0; margin-top: 20px;">⚖️ Lizenzhinweis:</h3>
|
||||||
|
<p style="background: rgba(255,204,0,0.1); padding: 10px; border-left: 3px solid #fc0;">
|
||||||
|
Dies ist ein <strong>Schulprojekt</strong>.<br>
|
||||||
|
Kommerzielle Nutzung und Veränderung des Quellcodes sind ausdrücklich untersagt.<br>
|
||||||
|
Alle Rechte liegen bei den Urhebern.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="back-btn" onclick="showMainMenu()">← ZURÜCK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DATENSCHUTZ MENU -->
|
||||||
|
<div id="datenschutzMenu" class="overlay-screen hidden">
|
||||||
|
<div class="center-box" style="max-width: 800px;">
|
||||||
|
<h1>🔒 DATENSCHUTZERKLÄRUNG</h1>
|
||||||
|
|
||||||
|
<div class="leaderboard-box" style="max-height: 500px; overflow-y: auto; text-align: left; font-family: sans-serif; font-size: 13px; line-height: 1.6; background: #333; color: #fff;">
|
||||||
|
<h3 style="color: #fc0; margin-top: 0;">1. Datenschutz auf einen Blick</h3>
|
||||||
|
<p><strong>Allgemeine Hinweise:</strong> Die folgenden Hinweise geben einen einfachen Überblick darüber, was mit Ihren personenbezogenen Daten passiert, wenn Sie diese Website besuchen.</p>
|
||||||
|
|
||||||
|
<h3 style="color: #fc0; margin-top: 20px;">2. Verantwortlicher</h3>
|
||||||
|
<p>Verantwortlich für die Datenverarbeitung auf dieser Website ist:<br>
|
||||||
|
<strong>Sebastian Unterschütz</strong><br>
|
||||||
|
Göltzschtalblick 16, 08236 Ellefeld<br>
|
||||||
|
E-Mail: sebastian@unterschuetz.de<br>
|
||||||
|
(Schulprojekt im Rahmen der IT232)</p>
|
||||||
|
|
||||||
|
<h3 style="color: #fc0; margin-top: 20px;">3. Hosting (Hetzner)</h3>
|
||||||
|
<p>Wir hosten die Inhalte unserer Website bei folgendem Anbieter:<br>
|
||||||
|
<strong>Hetzner Online GmbH</strong><br>
|
||||||
|
Industriestr. 25, 91710 Gunzenhausen, Deutschland</p>
|
||||||
|
<p><strong>Serverstandort:</strong> Deutschland (ausschließlich).<br>
|
||||||
|
Wir haben mit dem Anbieter einen Vertrag zur Auftragsverarbeitung (AVV) geschlossen, der die Einhaltung der DSGVO gewährleistet.</p>
|
||||||
|
|
||||||
|
<h3 style="color: #fc0; margin-top: 20px;">4. Datenerfassung auf dieser Website</h3>
|
||||||
|
|
||||||
|
<h4 style="color: #fc0; margin-top: 15px;">Server-Log-Dateien</h4>
|
||||||
|
<p>Der Provider der Seiten (Hetzner) erhebt und speichert automatisch Informationen in so genannten Server-Log-Dateien (Browser, OS, Referrer, Hostname, Uhrzeit, IP-Adresse).<br>
|
||||||
|
<strong>Rechtsgrundlage:</strong> Art. 6 Abs. 1 lit. f DSGVO (Berechtigtes Interesse an technischer Fehlerfreiheit und Sicherheit). Die Daten werden nach spätestens 14 Tagen gelöscht.</p>
|
||||||
|
|
||||||
|
<h4 style="color: #fc0; margin-top: 15px;">Spielstände & Highscores</h4>
|
||||||
|
<p>Wenn Sie einen Highscore eintragen, speichern wir:</p>
|
||||||
|
<ul style="margin-left: 20px;">
|
||||||
|
<li>Gewählter Name (Pseudonym empfohlen!)</li>
|
||||||
|
<li>Punktestand und Zeitstempel</li>
|
||||||
|
<li>Eindeutiger Player-Code (generiert)</li>
|
||||||
|
<li>Proof-Code (kryptografischer Hash zur Verifizierung des Scores)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Diese Daten dienen der Darstellung der Bestenliste und der Verifikation Ihrer Highscores.</p>
|
||||||
|
|
||||||
|
<h4 style="color: #fc0; margin-top: 15px;">Lokale Speicherung (LocalStorage)</h4>
|
||||||
|
<p>Das Spiel speichert folgende Daten lokal in Ihrem Browser:</p>
|
||||||
|
<ul style="margin-left: 20px;">
|
||||||
|
<li>Einstellungen (Audio-Lautstärke)</li>
|
||||||
|
<li>Ihr Spielername</li>
|
||||||
|
<li>Ihr Player-Code</li>
|
||||||
|
<li>Ihre erreichten Highscore-Codes mit Proof-Codes</li>
|
||||||
|
<li>Lokaler Highscore-Rekord</li>
|
||||||
|
</ul>
|
||||||
|
<p>Diese Daten verbleiben <strong>ausschließlich auf Ihrem Gerät</strong> und werden nicht an uns übertragen. Sie können diese Daten jederzeit über die Browser-Einstellungen löschen.</p>
|
||||||
|
|
||||||
|
<p style="background: rgba(255,204,0,0.1); padding: 10px; border-left: 3px solid #fc0; margin-top: 15px;">
|
||||||
|
<strong>Wichtig:</strong> Wir setzen <strong>keine Tracking-Cookies</strong> oder Analyse-Tools ein. Es erfolgt keine Weitergabe Ihrer Daten an Dritte (außer technisch notwendig über Hetzner als Hosting-Provider).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 style="color: #fc0; margin-top: 20px;">5. Ihre Rechte</h3>
|
||||||
|
<p>Sie haben jederzeit das Recht auf Auskunft, Berichtigung und Löschung Ihrer Daten. Wenden Sie sich dazu an den Verantwortlichen im Impressum.<br>
|
||||||
|
Um Ihre lokal gespeicherten Highscore-Codes zu löschen, nutzen Sie die Funktion im Menü <strong>"🔑 MEINE CODES"</strong> oder löschen Sie den Browser-LocalStorage.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="back-btn" onclick="showMainMenu()">← ZURÜCK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- LOBBY SCREEN (CO-OP WAITING ROOM) -->
|
<!-- LOBBY SCREEN (CO-OP WAITING ROOM) -->
|
||||||
<div id="lobbyScreen" class="overlay-screen hidden">
|
<div id="lobbyScreen" class="overlay-screen hidden">
|
||||||
<div class="center-box">
|
<div class="center-box">
|
||||||
@@ -134,6 +246,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="lobbyTeamNameBox" class="hidden" style="margin: 20px 0; width: 100%; max-width: 400px;">
|
||||||
|
<p style="font-size: 14px; color: #aaa; margin-bottom: 10px;">Team-Name (nur Host):</p>
|
||||||
|
<input type="text" id="lobbyTeamName" placeholder="TEAM-NAME EINGEBEN" maxlength="15" style="text-transform:uppercase; width: 100%; padding: 10px; font-size: 16px;">
|
||||||
|
<p id="currentTeamName" style="font-size: 12px; color: #ffcc00; margin-top: 5px;">Aktuell: <span id="teamNameDisplay">Nicht gesetzt</span></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="lobbyHostControls" class="hidden" style="margin: 20px 0;">
|
<div id="lobbyHostControls" class="hidden" style="margin: 20px 0;">
|
||||||
<button class="big-btn" onclick="startGameFromLobby()" style="background: #00cc00;">SPIEL STARTEN</button>
|
<button class="big-btn" onclick="startGameFromLobby()" style="background: #00cc00;">SPIEL STARTEN</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.2 MiB |
@@ -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')}
|
@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}
|
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}
|
#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{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}
|
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:rgba(0,0,0,.95);display:flex;justify-content:center;align-items:center;z-index:1000;box-sizing:border-box;padding:20px}
|
.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}
|
.overlay-screen.hidden{display:none!important}
|
||||||
#startScreen{display:flex;flex-direction:row;gap:40px;width:100%;height:100%;align-items:center;justify-content:center}
|
#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%}
|
.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:hover{background:#ffd700}
|
||||||
button:active{transform:translateY(4px);box-shadow:0 1px 0 #997a00}
|
button:active{transform:translateY(4px);box-shadow:0 1px 0 #997a00}
|
||||||
.big-btn{font-size:22px;padding:20px 40px}
|
.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{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:#333;color:#fff;border-color:#fff}
|
.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:0 0;border:1px solid #666;color:#888;box-shadow:none}
|
.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:#333;color:#fff;border-color:#fff}
|
.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]{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]::placeholder{color:#666}
|
||||||
input[type=text]:focus{border-color:#fc0;box-shadow:0 0 10px rgba(255,204,0,.5)}
|
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}
|
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-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}
|
.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}
|
.legal-bar{margin-top:20px;display:flex;gap:15px;flex-wrap:wrap;justify-content:center}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; padding: 0; background: red; }
|
|
||||||
.test-menu {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(0,0,0,0.9);
|
|
||||||
z-index: 1000;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: white;
|
|
||||||
font-size: 48px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="test-menu">TEST - SIEHST DU MICH?</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -25,8 +25,8 @@ import (
|
|||||||
|
|
||||||
// --- CONFIG ---
|
// --- CONFIG ---
|
||||||
const (
|
const (
|
||||||
AssetFile = "./cmd/client/assets/assets.json"
|
AssetFile = "cmd/client/web/assets/assets.json"
|
||||||
ChunkDir = "./cmd/client/assets/chunks"
|
ChunkDir = "cmd/client/web/assets/chunks"
|
||||||
|
|
||||||
LeftSidebarWidth = 250
|
LeftSidebarWidth = 250
|
||||||
RightSidebarWidth = 250
|
RightSidebarWidth = 250
|
||||||
@@ -116,7 +116,7 @@ func (le *LevelEditor) LoadAssets() {
|
|||||||
json.Unmarshal(data, &le.assetManifest)
|
json.Unmarshal(data, &le.assetManifest)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseDir := "./cmd/client/assets"
|
baseDir := "./cmd/client/web/assets"
|
||||||
le.assetList = []string{}
|
le.assetList = []string{}
|
||||||
|
|
||||||
for id, def := range le.assetManifest.Assets {
|
for id, def := range le.assetManifest.Assets {
|
||||||
|
|||||||
@@ -93,7 +93,25 @@ func main() {
|
|||||||
log.Printf("🔍 RAW NATS: Nachricht empfangen auf game.join: %s", string(m.Data))
|
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) {
|
_, _ = ec.Subscribe("game.input", func(input *game.ClientInput) {
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
room, ok := playerSessions[input.PlayerID]
|
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) {
|
_, _ = ec.Subscribe("score.submit", func(submission *game.ScoreSubmission) {
|
||||||
log.Printf("📊 Score-Submission: %s (%s) mit %d Punkten", submission.PlayerName, submission.PlayerCode, submission.Score)
|
// Verwende Team-Name wenn vorhanden (Coop-Mode), sonst Player-Name (Solo-Mode)
|
||||||
added := server.GlobalLeaderboard.AddScore(submission.PlayerName, submission.PlayerCode, submission.Score)
|
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 {
|
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{}) {
|
_, _ = ec.Subscribe("leaderboard.get", func(subject, reply string, _ *struct{}) {
|
||||||
top10 := server.GlobalLeaderboard.GetTop10()
|
top10 := server.GlobalLeaderboard.GetTop10()
|
||||||
log.Printf("📊 Leaderboard-Request beantwortet: %d Einträge", len(top10))
|
log.Printf("📊 Leaderboard-Request beantwortet: %d Einträge", len(top10))
|
||||||
ec.Publish(reply, 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) {
|
_, _ = ec.Subscribe("leaderboard.request", func(req *game.LeaderboardRequest) {
|
||||||
top10 := server.GlobalLeaderboard.GetTop10()
|
top10 := server.GlobalLeaderboard.GetTop10()
|
||||||
log.Printf("📊 Leaderboard-Request (Mode=%s): %d Einträge", req.Mode, len(top10))
|
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...")
|
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)
|
go StartWebSocketGateway("8080", ec)
|
||||||
|
|
||||||
// Block forever
|
// Block forever
|
||||||
@@ -145,7 +180,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadServerAssets(w *game.World) {
|
func loadServerAssets(w *game.World) {
|
||||||
assetDir := "./cmd/client/assets"
|
assetDir := "./cmd/client/web/assets"
|
||||||
chunkDir := filepath.Join(assetDir, "chunks")
|
chunkDir := filepath.Join(assetDir, "chunks")
|
||||||
|
|
||||||
// Manifest laden
|
// Manifest laden
|
||||||
|
|||||||
@@ -26,13 +26,15 @@ type WebSocketMessage struct {
|
|||||||
|
|
||||||
// WebSocketClient repräsentiert einen verbundenen WebSocket-Client
|
// WebSocketClient repräsentiert einen verbundenen WebSocket-Client
|
||||||
type WebSocketClient struct {
|
type WebSocketClient struct {
|
||||||
conn *websocket.Conn
|
conn *websocket.Conn
|
||||||
natsConn *nats.EncodedConn
|
natsConn *nats.EncodedConn
|
||||||
playerID string
|
playerID string
|
||||||
roomID string
|
playerCode string
|
||||||
send chan []byte
|
roomID string
|
||||||
mutex sync.Mutex
|
send chan []byte
|
||||||
subUpdates *nats.Subscription
|
mutex sync.Mutex
|
||||||
|
subUpdates *nats.Subscription
|
||||||
|
subScoreResp *nats.Subscription
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleWebSocket verwaltet eine WebSocket-Verbindung
|
// handleWebSocket verwaltet eine WebSocket-Verbindung
|
||||||
@@ -62,6 +64,9 @@ func (c *WebSocketClient) readPump() {
|
|||||||
if c.subUpdates != nil {
|
if c.subUpdates != nil {
|
||||||
c.subUpdates.Unsubscribe()
|
c.subUpdates.Unsubscribe()
|
||||||
}
|
}
|
||||||
|
if c.subScoreResp != nil {
|
||||||
|
c.subScoreResp.Unsubscribe()
|
||||||
|
}
|
||||||
c.conn.Close()
|
c.conn.Close()
|
||||||
log.Printf("🔌 WebSocket-Client getrennt: %s", c.conn.RemoteAddr())
|
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)
|
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)
|
c.natsConn.Publish("score.submit", &submit)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import "time"
|
|||||||
const (
|
const (
|
||||||
// Server Settings
|
// Server Settings
|
||||||
Port = ":8080"
|
Port = ":8080"
|
||||||
AssetPath = "./cmd/client/assets/assets.json"
|
AssetPath = "./cmd/client/web/assets/assets.json"
|
||||||
ChunkDir = "./cmd/client/assets/chunks"
|
ChunkDir = "./cmd/client/web/assets/chunks"
|
||||||
|
|
||||||
// Physics
|
// Physics
|
||||||
Gravity = 0.5
|
Gravity = 0.5
|
||||||
|
|||||||
@@ -66,10 +66,11 @@ type LoginPayload struct {
|
|||||||
|
|
||||||
// Input vom Spieler während des Spiels
|
// Input vom Spieler während des Spiels
|
||||||
type ClientInput struct {
|
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"`
|
RoomID string `json:"room_id"`
|
||||||
PlayerID string `json:"player_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 {
|
type JoinRequest struct {
|
||||||
@@ -105,6 +106,7 @@ type GameState struct {
|
|||||||
TimeLeft int `json:"time_left"`
|
TimeLeft int `json:"time_left"`
|
||||||
WorldChunks []ActiveChunk `json:"world_chunks"`
|
WorldChunks []ActiveChunk `json:"world_chunks"`
|
||||||
HostID string `json:"host_id"`
|
HostID string `json:"host_id"`
|
||||||
|
TeamName string `json:"team_name"` // Team-Name (vom Host gesetzt)
|
||||||
ScrollX float64 `json:"scroll_x"`
|
ScrollX float64 `json:"scroll_x"`
|
||||||
CollectedCoins map[string]bool `json:"collected_coins"` // Welche Coins wurden eingesammelt (Key: ChunkID_ObjectIndex)
|
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
|
CollectedPowerups map[string]bool `json:"collected_powerups"` // Welche Powerups wurden eingesammelt
|
||||||
@@ -125,7 +127,8 @@ type LeaderboardEntry struct {
|
|||||||
PlayerName string `json:"player_name"`
|
PlayerName string `json:"player_name"`
|
||||||
PlayerCode string `json:"player_code"` // Eindeutiger Code für Verifikation
|
PlayerCode string `json:"player_code"` // Eindeutiger Code für Verifikation
|
||||||
Score int `json:"score"`
|
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
|
// Score-Submission vom Client an Server
|
||||||
@@ -133,8 +136,16 @@ type ScoreSubmission struct {
|
|||||||
PlayerName string `json:"player_name"`
|
PlayerName string `json:"player_name"`
|
||||||
PlayerCode string `json:"player_code"`
|
PlayerCode string `json:"player_code"`
|
||||||
Score int `json:"score"`
|
Score int `json:"score"`
|
||||||
Name string `json:"name"` // Alternativer Name-Feld (für Kompatibilität)
|
Name string `json:"name"` // Alternativer Name-Feld (für Kompatibilität)
|
||||||
Mode string `json:"mode"` // "solo" oder "coop"
|
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
|
// Start-Request vom Client
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ func (w *World) GenerateColliders(activeChunks []ActiveChunk) []Collider {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if def.Type == "obstacle" || def.Type == "platform" {
|
if def.Type == "obstacle" || def.Type == "platform" || def.Type == "wall" {
|
||||||
c := Collider{
|
c := Collider{
|
||||||
Rect: Rect{
|
Rect: Rect{
|
||||||
OffsetX: ac.X + obj.X + def.DrawOffX + def.Hitbox.OffsetX,
|
OffsetX: ac.X + obj.X + def.DrawOffX + def.Hitbox.OffsetX,
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -39,17 +42,31 @@ func InitLeaderboard(redisAddr string) error {
|
|||||||
return nil
|
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
|
// Erstelle eindeutigen Key für diesen Score: PlayerCode + Timestamp
|
||||||
timestamp := time.Now().Unix()
|
timestamp := time.Now().Unix()
|
||||||
uniqueKey := code + "_" + time.Now().Format("20060102_150405")
|
uniqueKey := code + "_" + time.Now().Format("20060102_150405")
|
||||||
|
|
||||||
|
// Generiere Proof-Code
|
||||||
|
proofCode := GenerateProofCode(code, score, timestamp)
|
||||||
|
|
||||||
// Score speichern
|
// Score speichern
|
||||||
entry := game.LeaderboardEntry{
|
entry := game.LeaderboardEntry{
|
||||||
PlayerName: name,
|
PlayerName: name,
|
||||||
PlayerCode: code,
|
PlayerCode: code,
|
||||||
Score: score,
|
Score: score,
|
||||||
Timestamp: timestamp,
|
Timestamp: timestamp,
|
||||||
|
ProofCode: proofCode,
|
||||||
}
|
}
|
||||||
|
|
||||||
data, _ := json.Marshal(entry)
|
data, _ := json.Marshal(entry)
|
||||||
@@ -61,8 +78,8 @@ func (lb *Leaderboard) AddScore(name, code string, score int) bool {
|
|||||||
Member: uniqueKey,
|
Member: uniqueKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Printf("🏆 Leaderboard: %s mit %d Punkten (Entry: %s)", name, score, uniqueKey)
|
log.Printf("🏆 Leaderboard: %s mit %d Punkten (Entry: %s, Proof: %s)", name, score, uniqueKey, proofCode)
|
||||||
return true
|
return true, proofCode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (lb *Leaderboard) GetTop10() []game.LeaderboardEntry {
|
func (lb *Leaderboard) GetTop10() []game.LeaderboardEntry {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ type ServerPlayer struct {
|
|||||||
InputX float64 // -1 (Links), 0, 1 (Rechts)
|
InputX float64 // -1 (Links), 0, 1 (Rechts)
|
||||||
LastInputSeq uint32 // Letzte verarbeitete Input-Sequenz
|
LastInputSeq uint32 // Letzte verarbeitete Input-Sequenz
|
||||||
Score int
|
Score int
|
||||||
|
DistanceScore int // Score basierend auf zurückgelegter Distanz
|
||||||
|
BonusScore int // Score aus Coins und anderen Boni
|
||||||
IsAlive bool
|
IsAlive bool
|
||||||
IsSpectator bool
|
IsSpectator bool
|
||||||
|
|
||||||
@@ -68,9 +70,12 @@ type Room struct {
|
|||||||
Countdown int
|
Countdown int
|
||||||
NextStart time.Time
|
NextStart time.Time
|
||||||
HostID string
|
HostID string
|
||||||
|
TeamName string // Name des Teams (vom Host gesetzt)
|
||||||
CollectedCoins map[string]bool // Key: "chunkID_objectIndex"
|
CollectedCoins map[string]bool // Key: "chunkID_objectIndex"
|
||||||
CollectedPowerups map[string]bool // Key: "chunkID_objectIndex"
|
CollectedPowerups map[string]bool // Key: "chunkID_objectIndex"
|
||||||
ScoreAccum float64 // Akkumulator für Distanz-Score
|
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
|
// Chunk-Pool für fairen Random-Spawn
|
||||||
ChunkPool []string // Verfügbare Chunks für nächsten 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),
|
CollectedCoins: make(map[string]bool),
|
||||||
CollectedPowerups: make(map[string]bool),
|
CollectedPowerups: make(map[string]bool),
|
||||||
ChunkSpawnedCount: make(map[string]int),
|
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
|
// 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" {
|
if input.PlayerID == r.HostID && r.Status == "LOBBY" {
|
||||||
r.StartCountdown()
|
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
|
r.Countdown = int(rem.Seconds()) + 1
|
||||||
if rem <= 0 {
|
if rem <= 0 {
|
||||||
r.Status = "RUNNING"
|
r.Status = "RUNNING"
|
||||||
|
r.GameStartTime = time.Now()
|
||||||
|
r.CurrentSpeed = config.RunSpeed
|
||||||
}
|
}
|
||||||
} else if r.Status == "RUNNING" {
|
} 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
|
// Bewegende Plattformen updaten
|
||||||
r.UpdateMovingPlatforms()
|
r.UpdateMovingPlatforms()
|
||||||
}
|
}
|
||||||
@@ -333,9 +355,9 @@ func (r *Room) Update() {
|
|||||||
|
|
||||||
// X Bewegung
|
// X Bewegung
|
||||||
// Symmetrische Geschwindigkeit: Links = Rechts
|
// Symmetrische Geschwindigkeit: Links = Rechts
|
||||||
// Nach rechts: RunSpeed + 11, Nach links: RunSpeed - 11
|
// Nach rechts: CurrentSpeed + 11, Nach links: CurrentSpeed - 11
|
||||||
// Ergebnis: Rechts = 18, Links = -4 (beide gleich weit vom Scroll)
|
// Verwendet r.CurrentSpeed statt config.RunSpeed für dynamische Geschwindigkeit
|
||||||
currentSpeed := config.RunSpeed + (p.InputX * 11.0)
|
currentSpeed := r.CurrentSpeed + (p.InputX * 11.0)
|
||||||
nextX := p.X + currentSpeed
|
nextX := p.X + currentSpeed
|
||||||
|
|
||||||
hitX, typeX := r.CheckCollision(nextX+r.pDrawOffX+r.pHitboxOffX, p.Y+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH)
|
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" {
|
if typeX == "wall" {
|
||||||
// Wand getroffen - kann klettern!
|
// Wand getroffen - kann klettern!
|
||||||
p.OnWall = true
|
p.OnWall = true
|
||||||
// X-Position nicht ändern (bleibt an der Wand)
|
// X-Position NICHT ändern (bleibt vor der Wand stehen)
|
||||||
} else if typeX == "obstacle" {
|
} else if typeX == "obstacle" {
|
||||||
// Godmode prüfen
|
// Godmode prüfen
|
||||||
if p.HasGodMode && time.Now().Before(p.GodModeEndTime) {
|
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)
|
hitY, typeY := r.CheckCollision(p.X+r.pDrawOffX+r.pHitboxOffX, nextY+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH)
|
||||||
if hitY {
|
if hitY {
|
||||||
if typeY == "wall" {
|
if typeY == "wall" {
|
||||||
// An der Wand: Nicht töten, sondern Position halten
|
// An der Wand: Nicht töten, Position halten und klettern ermöglichen
|
||||||
if p.OnWall {
|
p.VY = 0
|
||||||
p.VY = 0
|
p.OnWall = true
|
||||||
} else {
|
|
||||||
// Von oben/unten gegen Wand - töten (kein Klettern in Y-Richtung)
|
|
||||||
p.Y = nextY
|
|
||||||
r.KillPlayer(p)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else if typeY == "obstacle" {
|
} else if typeY == "obstacle" {
|
||||||
// Obstacle - immer töten
|
// Obstacle - immer töten
|
||||||
p.Y = nextY
|
p.Y = nextY
|
||||||
@@ -774,6 +790,7 @@ func (r *Room) Broadcast() {
|
|||||||
TimeLeft: r.Countdown,
|
TimeLeft: r.Countdown,
|
||||||
WorldChunks: r.ActiveChunks,
|
WorldChunks: r.ActiveChunks,
|
||||||
HostID: r.HostID,
|
HostID: r.HostID,
|
||||||
|
TeamName: r.TeamName,
|
||||||
ScrollX: r.GlobalScrollX,
|
ScrollX: r.GlobalScrollX,
|
||||||
CollectedCoins: r.CollectedCoins,
|
CollectedCoins: r.CollectedCoins,
|
||||||
CollectedPowerups: r.CollectedPowerups,
|
CollectedPowerups: r.CollectedPowerups,
|
||||||
|
|||||||
@@ -60,7 +60,8 @@ func (r *Room) CheckCoinCollision(p *ServerPlayer) {
|
|||||||
if game.CheckRectCollision(playerHitbox, coinHitbox) {
|
if game.CheckRectCollision(playerHitbox, coinHitbox) {
|
||||||
// Coin einsammeln!
|
// Coin einsammeln!
|
||||||
r.CollectedCoins[coinKey] = true
|
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)
|
log.Printf("💰 %s hat Coin eingesammelt! Score: %d", p.Name, p.Score)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,18 +144,28 @@ func (r *Room) UpdateDistanceScore() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jeder Spieler bekommt Punkte basierend auf seiner eigenen Distanz
|
// Zähle lebende Spieler
|
||||||
// Punkte = (X-Position / TileSize) = Distanz in Tiles
|
aliveCount := 0
|
||||||
for _, p := range r.Players {
|
for _, p := range r.Players {
|
||||||
if p.IsAlive && !p.IsSpectator {
|
if p.IsAlive && !p.IsSpectator {
|
||||||
// Berechne Score basierend auf X-Position
|
aliveCount++
|
||||||
// 1 Punkt pro Tile (64px)
|
}
|
||||||
newScore := int(p.X / 64.0)
|
}
|
||||||
|
|
||||||
// Nur updaten wenn höher als aktueller Score
|
if aliveCount == 0 {
|
||||||
if newScore > p.Score {
|
return
|
||||||
p.Score = newScore
|
}
|
||||||
}
|
|
||||||
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||