Private
Public Access
1
0
Files
EscapeFromTeacher/cmd/client/offline_logic.go
Sebastian Unterschütz 9742ccb038
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m58s
enhance offline mode: add moving platforms, collision detection, and power-ups
2026-04-22 18:35:58 +02:00

341 lines
8.9 KiB
Go

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
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,
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!")
}
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
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
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
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:]
}
}
// 5. Update Moving Platforms
g.updateOfflineMovingPlatforms()
// 6. 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 Moving Platforms 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 && asset.Type == "moving_platform" && obj.MovingPlatform != nil {
mp := obj.MovingPlatform
p := &MovingPlatform{
ChunkID: randomID,
ObjectIdx: i,
AssetID: obj.AssetID,
StartX: atX + mp.StartX,
StartY: mp.StartY,
EndX: atX + mp.EndX,
EndY: mp.EndY,
Speed: mp.Speed,
Direction: 1.0,
IsActive: true,
CurrentX: atX + mp.StartX,
CurrentY: mp.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)
}
}
}
}
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)
}
}
}
}
}
}