add offline mode for solo play with local game state simulation
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m47s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m47s
This commit is contained in:
@@ -185,9 +185,14 @@ func (g *Game) UpdateGame() {
|
||||
}
|
||||
|
||||
// --- 5. INPUT SENDEN (MIT CLIENT PREDICTION, 20 TPS) ---
|
||||
if g.connected && time.Since(g.lastInputTime) >= physicsStep {
|
||||
if (g.connected || g.isOffline) && time.Since(g.lastInputTime) >= physicsStep {
|
||||
g.lastInputTime = time.Now()
|
||||
|
||||
// Offline: Update Scroll & World logic locally
|
||||
if g.isOffline {
|
||||
g.updateOfflineLoop()
|
||||
}
|
||||
|
||||
g.predictionMutex.Lock()
|
||||
wasOnGround := g.predictedGround
|
||||
g.predictionMutex.Unlock()
|
||||
|
||||
@@ -27,8 +27,10 @@ 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" {
|
||||
if !g.scoreSubmitted && g.gameMode == "solo" && !g.isOffline {
|
||||
g.submitScore() // submitScore() ruft requestLeaderboard() auf
|
||||
} else if !g.scoreSubmitted && g.gameMode == "solo" && g.isOffline {
|
||||
// Offline-Solo: Keine automatische Submission
|
||||
} else {
|
||||
// Für Coop: Nur Leaderboard anfordern, nicht submitten
|
||||
g.leaderboardMutex.Lock()
|
||||
@@ -169,6 +171,9 @@ func (g *Game) drawGameOverScreen(screen *ebiten.Image, myScore int) {
|
||||
} else if g.gameMode == "solo" && g.scoreSubmitted {
|
||||
// Solo: Zeige Bestätigungsmeldung
|
||||
text.Draw(screen, "✓ Runde verifiziert & Score eingereicht!", basicfont.Face7x13, ScreenWidth/2-110, ScreenHeight-100, color.RGBA{0, 255, 0, 255})
|
||||
} else if g.gameMode == "solo" && g.isOffline {
|
||||
// Offline Solo
|
||||
text.Draw(screen, "Offline-Modus: Score lokal gespeichert.", basicfont.Face7x13, ScreenWidth/2-110, ScreenHeight-100, color.RGBA{200, 200, 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})
|
||||
|
||||
@@ -73,6 +73,7 @@ type Game struct {
|
||||
roomID string
|
||||
activeField string // "name" oder "room" oder "teamname"
|
||||
gameMode string // "solo" oder "coop"
|
||||
isOffline bool // Läuft das Spiel lokal ohne Server?
|
||||
isHost bool
|
||||
teamName string // Team-Name für Coop beim Game Over
|
||||
|
||||
@@ -351,15 +352,13 @@ func (g *Game) updateMenu() {
|
||||
btnY := ScreenHeight/2 - 20
|
||||
|
||||
if isHit(soloX, btnY, btnW, btnH) {
|
||||
// SOLO MODE
|
||||
// SOLO MODE (Offline by default)
|
||||
if g.playerName == "" {
|
||||
g.playerName = "Player"
|
||||
}
|
||||
g.gameMode = "solo"
|
||||
g.roomID = fmt.Sprintf("solo_%d", time.Now().UnixNano())
|
||||
g.isHost = true
|
||||
g.appState = StateGame
|
||||
go g.connectAndStart()
|
||||
g.startOfflineGame()
|
||||
} else if isHit(coopX, btnY, btnW, btnH) {
|
||||
// CO-OP MODE
|
||||
if g.playerName == "" {
|
||||
@@ -926,6 +925,10 @@ func (g *Game) SendCommand(cmdType string) {
|
||||
|
||||
func (g *Game) SendInputWithSequence(input InputState) {
|
||||
if !g.connected {
|
||||
// Im Offline-Modus den Jump-Sound trotzdem lokal abspielen
|
||||
if input.Jump && g.isOffline {
|
||||
g.audio.PlayJump()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
128
cmd/client/offline_logic.go
Normal file
128
cmd/client/offline_logic.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/config"
|
||||
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game"
|
||||
)
|
||||
|
||||
// startOfflineGame initialisiert eine lokale Spielrunde ohne Server
|
||||
func (g *Game) startOfflineGame() {
|
||||
g.resetForNewGame()
|
||||
g.isOffline = true
|
||||
g.connected = false // Explizit offline
|
||||
g.appState = StateGame
|
||||
|
||||
// Initialen GameState lokal erstellen
|
||||
g.stateMutex.Lock()
|
||||
g.gameState = game.GameState{
|
||||
Status: "RUNNING",
|
||||
RoomID: "offline_solo",
|
||||
Players: make(map[string]game.PlayerState),
|
||||
WorldChunks: []game.ActiveChunk{{ChunkID: "start", X: 0}},
|
||||
CurrentSpeed: config.RunSpeed,
|
||||
DifficultyFactor: 0,
|
||||
}
|
||||
|
||||
// Lokalen Spieler hinzufügen
|
||||
g.gameState.Players[g.playerName] = game.PlayerState{
|
||||
ID: g.playerName,
|
||||
Name: g.playerName,
|
||||
X: 100,
|
||||
Y: 200,
|
||||
IsAlive: true,
|
||||
}
|
||||
g.stateMutex.Unlock()
|
||||
|
||||
// Initialer Chunk-Library Check
|
||||
if len(g.world.ChunkLibrary) == 0 {
|
||||
log.Println("⚠️ Warnung: Keine Chunks in Library geladen!")
|
||||
}
|
||||
|
||||
g.roundStartTime = time.Now()
|
||||
g.predictedX = 100
|
||||
g.predictedY = 200
|
||||
g.currentSpeed = config.RunSpeed
|
||||
|
||||
g.audio.PlayMusic()
|
||||
g.notifyGameStarted()
|
||||
log.Println("🕹️ Offline-Modus gestartet")
|
||||
}
|
||||
|
||||
// updateOfflineLoop simuliert die Server-Logik lokal
|
||||
func (g *Game) updateOfflineLoop() {
|
||||
if !g.isOffline || g.gameState.Status != "RUNNING" {
|
||||
return
|
||||
}
|
||||
|
||||
g.stateMutex.Lock()
|
||||
defer g.stateMutex.Unlock()
|
||||
|
||||
elapsed := time.Since(g.roundStartTime).Seconds()
|
||||
|
||||
// 1. Schwierigkeit & Speed (analog zu pkg/server/room.go)
|
||||
g.gameState.DifficultyFactor = elapsed / config.MaxDifficultySeconds
|
||||
if g.gameState.DifficultyFactor > 1.0 {
|
||||
g.gameState.DifficultyFactor = 1.0
|
||||
}
|
||||
|
||||
speedIncrease := g.gameState.DifficultyFactor * g.gameState.DifficultyFactor * 18.0
|
||||
g.gameState.CurrentSpeed = config.RunSpeed + speedIncrease
|
||||
g.currentSpeed = g.gameState.CurrentSpeed
|
||||
|
||||
// 2. Scrolling
|
||||
g.gameState.ScrollX += g.currentSpeed
|
||||
|
||||
// 3. Chunks nachladen
|
||||
// Wenn das Ende der Map nah am rechten Rand ist, neuen Chunk spawnen
|
||||
mapEnd := 0.0
|
||||
for _, c := range g.gameState.WorldChunks {
|
||||
chunkDef := g.world.ChunkLibrary[c.ChunkID]
|
||||
end := c.X + float64(chunkDef.Width*config.TileSize)
|
||||
if end > mapEnd {
|
||||
mapEnd = end
|
||||
}
|
||||
}
|
||||
|
||||
if mapEnd < g.gameState.ScrollX+2500 {
|
||||
g.spawnOfflineChunk(mapEnd)
|
||||
}
|
||||
|
||||
// 4. Entferne alte Chunks (links aus dem Bild)
|
||||
if len(g.gameState.WorldChunks) > 5 {
|
||||
// Behalte immer mindestens die letzten paar Chunks
|
||||
if g.gameState.WorldChunks[0].X < g.gameState.ScrollX-2000 {
|
||||
g.gameState.WorldChunks = g.gameState.WorldChunks[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Score Update (Distanz)
|
||||
p, ok := g.gameState.Players[g.playerName]
|
||||
if ok && p.IsAlive {
|
||||
// Grobe Score-Simulation
|
||||
p.Score = int(g.gameState.ScrollX / 10)
|
||||
g.gameState.Players[g.playerName] = p
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) spawnOfflineChunk(atX float64) {
|
||||
// Zufälligen Chunk wählen
|
||||
var pool []string
|
||||
for id := range g.world.ChunkLibrary {
|
||||
if id != "start" {
|
||||
pool = append(pool)
|
||||
pool = append(pool, id)
|
||||
}
|
||||
}
|
||||
|
||||
if len(pool) > 0 {
|
||||
randomID := pool[rand.Intn(len(pool))]
|
||||
g.gameState.WorldChunks = append(g.gameState.WorldChunks, game.ActiveChunk{
|
||||
ChunkID: randomID,
|
||||
X: atX,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user