All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m50s
368 lines
9.6 KiB
Go
368 lines
9.6 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 (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]
|
|
// In Solo gibt es keine MovingPlatformData, Plattformen sind statisch
|
|
if ok && asset.Type == "moving_platform" {
|
|
p := &MovingPlatform{
|
|
ChunkID: randomID,
|
|
ObjectIdx: i,
|
|
AssetID: obj.AssetID,
|
|
StartX: atX + obj.X,
|
|
StartY: obj.Y,
|
|
EndX: atX + obj.X,
|
|
EndY: obj.Y,
|
|
Speed: 0,
|
|
Direction: 1.0,
|
|
IsActive: false,
|
|
CurrentX: atX + obj.X,
|
|
CurrentY: obj.Y,
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|