package main import ( "fmt" "log" "math" "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 (mit Countdown) g.stateMutex.Lock() g.gameState = game.GameState{ Status: "COUNTDOWN", TimeLeft: 3, RoomID: "offline_solo", Players: make(map[string]game.PlayerState), WorldChunks: []game.ActiveChunk{{ChunkID: "start", X: 0}}, CurrentSpeed: config.RunSpeed, DifficultyFactor: 0, CollectedCoins: make(map[string]bool), CollectedPowerups: make(map[string]bool), } // 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() g.offlineMovingPlatforms = nil // Initialer Chunk-Library Check if len(g.world.ChunkLibrary) == 0 { log.Println("⚠️ Warnung: Keine Chunks in Library geladen!") } // Startzeit für Countdown g.roundStartTime = time.Now().Add(3 * time.Second) g.predictedX = 100 g.predictedY = 200 g.currentSpeed = 0 // Stillstand während Countdown g.notifyGameStarted() log.Println("🕹️ Offline-Modus mit Countdown gestartet") } // updateOfflineLoop simuliert die Server-Logik lokal func (g *Game) updateOfflineLoop() { if !g.isOffline || g.gameState.Status == "GAMEOVER" { return } g.stateMutex.Lock() defer g.stateMutex.Unlock() // 1. Status Logic (Countdown -> Running) if g.gameState.Status == "COUNTDOWN" { rem := time.Until(g.roundStartTime) g.gameState.TimeLeft = int(rem.Seconds()) + 1 if rem <= 0 { log.Println("🚀 Offline: GO!") g.gameState.Status = "RUNNING" g.gameState.TimeLeft = 0 g.audio.PlayMusic() // Reset roundStartTime auf den tatsächlichen Spielstart für Schwierigkeits-Skalierung g.roundStartTime = time.Now() } return // Während Countdown keine weitere Logik (kein Scrolling, etc.) } if g.gameState.Status != "RUNNING" { return } elapsed := time.Since(g.roundStartTime).Seconds() // 2. Schwierigkeit & Speed 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 // 3. Scrolling g.gameState.ScrollX += g.currentSpeed // 4. Chunks nachladen 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) } // 5. Entferne alte Chunks if len(g.gameState.WorldChunks) > 5 { if g.gameState.WorldChunks[0].X < g.gameState.ScrollX-2000 { // Bereinige auch Moving Platforms des alten Chunks oldChunkID := g.gameState.WorldChunks[0].ChunkID newPlats := g.offlineMovingPlatforms[:0] for _, p := range g.offlineMovingPlatforms { if p.ChunkID != oldChunkID { newPlats = append(newPlats, p) } } g.offlineMovingPlatforms = newPlats g.gameState.WorldChunks = g.gameState.WorldChunks[1:] } } // 6. Update Moving Platforms g.updateOfflineMovingPlatforms() // 7. Player State Update (Score, Powerups, Collisions) p, ok := g.gameState.Players[g.playerName] if ok && p.IsAlive { // Basis-Score aus Distanz p.Score = int(g.gameState.ScrollX / 10) // Synchronisiere Prediction-State zurück in GameState (für Rendering) p.X = g.predictedX p.Y = g.predictedY p.VX = g.predictedVX p.VY = g.predictedVY p.OnGround = g.predictedGround p.OnWall = g.predictedOnWall // Lokale Kollisionsprüfung für Coins/Powerups g.checkOfflineCollisions(&p) // Powerup-Timer herunterschalten now := time.Now() if p.HasGodMode && now.After(g.godModeEndTime) { p.HasGodMode = false } if p.HasMagnet && now.After(g.magnetEndTime) { p.HasMagnet = false } if g.predictedHasDoubleJump && now.After(g.doubleJumpEndTime) { g.predictedHasDoubleJump = false p.HasDoubleJump = false } g.gameState.Players[g.playerName] = p } // Synchronisiere Plattform-Positionen für Renderer syncPlats := make([]game.MovingPlatformSync, len(g.offlineMovingPlatforms)) for i, p := range g.offlineMovingPlatforms { syncPlats[i] = game.MovingPlatformSync{ ChunkID: p.ChunkID, ObjectIdx: p.ObjectIdx, AssetID: p.AssetID, X: p.CurrentX, Y: p.CurrentY, } } g.gameState.MovingPlatforms = syncPlats } func (g *Game) spawnOfflineChunk(atX float64) { var pool []string for id := range g.world.ChunkLibrary { if id != "start" { 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, }) // Extrahiere Plattformen aus dem neuen Chunk chunkDef := g.world.ChunkLibrary[randomID] for i, obj := range chunkDef.Objects { asset, ok := g.world.Manifest.Assets[obj.AssetID] if !ok { continue } // Check ob es eine bewegende Plattform ist (entweder Typ oder explizite Daten) if obj.MovingPlatform != nil { mpData := obj.MovingPlatform p := &MovingPlatform{ ChunkID: randomID, ObjectIdx: i, AssetID: obj.AssetID, StartX: atX + mpData.StartX, StartY: mpData.StartY, EndX: atX + mpData.EndX, EndY: mpData.EndY, Speed: mpData.Speed, Direction: 1.0, IsActive: true, CurrentX: atX + mpData.StartX, CurrentY: mpData.StartY, HitboxW: asset.Hitbox.W, HitboxH: asset.Hitbox.H, DrawOffX: asset.DrawOffX, DrawOffY: asset.DrawOffY, HitboxOffX: asset.Hitbox.OffsetX, HitboxOffY: asset.Hitbox.OffsetY, } g.offlineMovingPlatforms = append(g.offlineMovingPlatforms, p) } else if asset.Type == "moving_platform" || asset.Type == "platform" { // Statische Plattform (oder Fallback) // Wir fügen sie NICHT zu offlineMovingPlatforms hinzu, da sie über // den statischen World-Collider Check in physics.go bereits erfasst wird. // (Vorausgesetzt der Typ ist "platform") } } } } func (g *Game) updateOfflineMovingPlatforms() { for _, p := range g.offlineMovingPlatforms { if !p.IsActive { continue } dx := p.EndX - p.StartX dy := p.EndY - p.StartY dist := math.Sqrt(dx*dx + dy*dy) if dist < 1 { continue } vx := (dx / dist) * (p.Speed / 20.0) * p.Direction vy := (dy / dist) * (p.Speed / 20.0) * p.Direction p.CurrentX += vx p.CurrentY += vy // Ziel erreicht? Umkehren. if p.Direction > 0 { dToEnd := math.Sqrt(math.Pow(p.CurrentX-p.EndX, 2) + math.Pow(p.CurrentY-p.EndY, 2)) if dToEnd < (p.Speed / 20.0) { p.Direction = -1.0 } } else { dToStart := math.Sqrt(math.Pow(p.CurrentX-p.StartX, 2) + math.Pow(p.CurrentY-p.StartY, 2)) if dToStart < (p.Speed / 20.0) { p.Direction = 1.0 } } } } func (g *Game) checkOfflineCollisions(p *game.PlayerState) { // Hitbox des Spielers (Welt-Koordinaten) pW, pH := 40.0, 60.0 // Default pOffX, pOffY := 0.0, 0.0 pDrawX, pDrawY := 0.0, 0.0 if def, ok := g.world.Manifest.Assets["player"]; ok { pW = def.Hitbox.W pH = def.Hitbox.H pOffX = def.Hitbox.OffsetX pOffY = def.Hitbox.OffsetY pDrawX = def.DrawOffX pDrawY = def.DrawOffY } pRect := game.Rect{ OffsetX: p.X + pDrawX + pOffX, OffsetY: p.Y + pDrawY + pOffY, W: pW, H: pH, } for _, ac := range g.gameState.WorldChunks { chunkDef := g.world.ChunkLibrary[ac.ChunkID] for i, obj := range chunkDef.Objects { asset, ok := g.world.Manifest.Assets[obj.AssetID] if !ok { continue } objID := fmt.Sprintf("%s_%d", ac.ChunkID, i) // 1. COINS if asset.Type == "coin" { if g.gameState.CollectedCoins[objID] { continue } coinX := ac.X + obj.X + asset.DrawOffX + asset.Hitbox.OffsetX coinY := obj.Y + asset.DrawOffY + asset.Hitbox.OffsetY // Magnet-Effekt? if p.HasMagnet { playerCenterX := pRect.OffsetX + pRect.W/2 playerCenterY := pRect.OffsetY + pRect.H/2 coinCenterX := coinX + asset.Hitbox.W/2 coinCenterY := coinY + asset.Hitbox.H/2 dist := math.Sqrt(math.Pow(playerCenterX-coinCenterX, 2) + math.Pow(playerCenterY-coinCenterY, 2)) if dist < 300 { // Münze wird eingesammelt wenn im Magnet-Radius g.gameState.CollectedCoins[objID] = true p.Score += 200 // Bonus direkt auf Score g.audio.PlayCoin() continue } } coinRect := game.Rect{OffsetX: coinX, OffsetY: coinY, W: asset.Hitbox.W, H: asset.Hitbox.H} if game.CheckRectCollision(pRect, coinRect) { g.gameState.CollectedCoins[objID] = true p.Score += 200 g.audio.PlayCoin() } } // 2. POWERUPS if asset.Type == "powerup" { if g.gameState.CollectedPowerups[objID] { continue } puRect := game.Rect{ OffsetX: ac.X + obj.X + asset.DrawOffX + asset.Hitbox.OffsetX, OffsetY: obj.Y + asset.DrawOffY + asset.Hitbox.OffsetY, W: asset.Hitbox.W, H: asset.Hitbox.H, } if game.CheckRectCollision(pRect, puRect) { g.gameState.CollectedPowerups[objID] = true g.audio.PlayPowerUp() switch obj.AssetID { case "jumpboost": p.HasDoubleJump = true p.DoubleJumpUsed = false g.predictedHasDoubleJump = true g.predictedDoubleJumpUsed = false g.doubleJumpEndTime = time.Now().Add(15 * time.Second) case "godmode": p.HasGodMode = true g.godModeEndTime = time.Now().Add(10 * time.Second) case "magnet": p.HasMagnet = true g.magnetEndTime = time.Now().Add(8 * time.Second) } } } } } }