package server import ( "log" "math/rand" "sync" "time" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/config" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" "github.com/nats-io/nats.go" ) type ServerPlayer struct { ID string Name string X, Y float64 VX, VY float64 OnGround bool InputX float64 // -1 (Links), 0, 1 (Rechts) } type Room struct { ID string NC *nats.Conn World *game.World Mutex sync.RWMutex Players map[string]*ServerPlayer ActiveChunks []game.ActiveChunk Colliders []game.Collider Status string // "LOBBY", "COUNTDOWN", "RUNNING", "GAMEOVER" GlobalScrollX float64 MapEndX float64 Countdown int NextStart time.Time HostID string stopChan chan struct{} // Cache für Spieler-Hitbox aus Assets pW, pH float64 pDrawOffX float64 pDrawOffY float64 pHitboxOffX float64 pHitboxOffY float64 } // Konstruktor func NewRoom(id string, nc *nats.Conn, w *game.World) *Room { r := &Room{ ID: id, NC: nc, World: w, Players: make(map[string]*ServerPlayer), Status: "LOBBY", stopChan: make(chan struct{}), pW: 40, pH: 60, // Fallback } // Player Werte aus Manifest laden if def, ok := w.Manifest.Assets["player"]; ok { r.pW = def.Hitbox.W r.pH = def.Hitbox.H r.pDrawOffX = def.DrawOffX r.pDrawOffY = def.DrawOffY r.pHitboxOffX = def.Hitbox.OffsetX r.pHitboxOffY = def.Hitbox.OffsetY } // Start-Chunk startChunk := game.ActiveChunk{ChunkID: "start", X: 0} r.ActiveChunks = append(r.ActiveChunks, startChunk) r.MapEndX = 1280 // Erste Chunks generieren r.SpawnNextChunk() r.SpawnNextChunk() r.Colliders = r.World.GenerateColliders(r.ActiveChunks) return r } // --- MAIN LOOP --- func (r *Room) RunLoop() { // 60 Tick pro Sekunde ticker := time.NewTicker(time.Second / 60) defer ticker.Stop() for { select { case <-r.stopChan: return case <-ticker.C: r.Update() r.Broadcast() } } } // --- PLAYER MANAGEMENT --- func (r *Room) AddPlayer(id, name string) { r.Mutex.Lock() defer r.Mutex.Unlock() if _, exists := r.Players[id]; exists { return } p := &ServerPlayer{ ID: id, Name: name, X: 100, Y: 200, OnGround: false, } // Falls das Spiel schon läuft, spawnen wir weiter rechts if r.Status == "RUNNING" { p.X = r.GlobalScrollX + 200 p.Y = 200 } r.Players[id] = p // Erster Spieler wird Host if r.HostID == "" { r.HostID = id } } func (r *Room) ResetPlayer(p *ServerPlayer) { p.Y = 200 p.X = r.GlobalScrollX + 200 // Sicherer Spawn p.VY = 0 p.VX = 0 p.OnGround = false log.Printf("♻️ RESET Player %s", p.Name) } // --- INPUT HANDLER --- func (r *Room) HandleInput(input game.ClientInput) { r.Mutex.Lock() defer r.Mutex.Unlock() p, exists := r.Players[input.PlayerID] if !exists { return } switch input.Type { case "JUMP": if p.OnGround { p.VY = -14.0 p.OnGround = false } case "DOWN": p.VY = 15.0 case "LEFT_DOWN": p.InputX = -1 case "LEFT_UP": if p.InputX == -1 { p.InputX = 0 } case "RIGHT_DOWN": p.InputX = 1 case "RIGHT_UP": if p.InputX == 1 { p.InputX = 0 } case "START": if input.PlayerID == r.HostID && r.Status == "LOBBY" { r.StartCountdown() } } } func (r *Room) StartCountdown() { r.Status = "COUNTDOWN" r.NextStart = time.Now().Add(3 * time.Second) } // --- PHYSIK & UPDATE --- func (r *Room) Update() { r.Mutex.Lock() defer r.Mutex.Unlock() // 1. Status Logic if r.Status == "COUNTDOWN" { rem := time.Until(r.NextStart) r.Countdown = int(rem.Seconds()) + 1 if rem <= 0 { r.Status = "RUNNING" } } else if r.Status == "RUNNING" { r.GlobalScrollX += config.RunSpeed } maxX := r.GlobalScrollX // 2. Spieler Physik for _, p := range r.Players { // Lobby Mode if r.Status != "RUNNING" { p.VY += config.Gravity if p.Y > 540 { p.Y = 540 p.VY = 0 p.OnGround = true } if p.X < r.GlobalScrollX+50 { p.X = r.GlobalScrollX + 50 } continue } // X Bewegung currentSpeed := config.RunSpeed + (p.InputX * 4.0) nextX := p.X + currentSpeed hitX, typeX := r.CheckCollision(nextX+r.pDrawOffX+r.pHitboxOffX, p.Y+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH) if hitX { if typeX == "obstacle" { r.ResetPlayer(p) continue } } else { p.X = nextX } // Grenzen if p.X > r.GlobalScrollX+1200 { p.X = r.GlobalScrollX + 1200 } // Rechts Block if p.X < r.GlobalScrollX-50 { r.ResetPlayer(p) continue } // Links Tod if p.X > maxX { maxX = p.X } // Y Bewegung p.VY += config.Gravity if p.VY > config.MaxFall { p.VY = config.MaxFall } nextY := p.Y + p.VY hitY, typeY := r.CheckCollision(p.X+r.pDrawOffX+r.pHitboxOffX, nextY+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH) if hitY { if typeY == "obstacle" { r.ResetPlayer(p) continue } if p.VY > 0 { p.OnGround = true } p.VY = 0 } else { p.Y += p.VY p.OnGround = false } if p.Y > 1000 { r.ResetPlayer(p) } } // 3. Map Management r.UpdateMapLogic(maxX) // 4. Host Check if _, ok := r.Players[r.HostID]; !ok && len(r.Players) > 0 { for id := range r.Players { r.HostID = id break } } } // --- COLLISION & MAP --- func (r *Room) CheckCollision(x, y, w, h float64) (bool, string) { playerRect := game.Rect{OffsetX: x, OffsetY: y, W: w, H: h} for _, c := range r.Colliders { if game.CheckRectCollision(playerRect, c.Rect) { return true, c.Type } } return false, "" } func (r *Room) UpdateMapLogic(maxX float64) { if r.Status != "RUNNING" { return } // Neue Chunks spawnen if maxX > r.MapEndX-2000 { r.SpawnNextChunk() r.Colliders = r.World.GenerateColliders(r.ActiveChunks) } // Alte Chunks löschen if len(r.ActiveChunks) > 0 { firstChunk := r.ActiveChunks[0] chunkDef := r.World.ChunkLibrary[firstChunk.ChunkID] chunkWidth := float64(chunkDef.Width * config.TileSize) if firstChunk.X+chunkWidth < r.GlobalScrollX-1000 { r.ActiveChunks = r.ActiveChunks[1:] r.Colliders = r.World.GenerateColliders(r.ActiveChunks) } } } func (r *Room) SpawnNextChunk() { keys := make([]string, 0, len(r.World.ChunkLibrary)) for k := range r.World.ChunkLibrary { keys = append(keys, k) } if len(keys) > 0 { // Zufälligen Chunk wählen randomID := keys[rand.Intn(len(keys))] chunkDef := r.World.ChunkLibrary[randomID] newChunk := game.ActiveChunk{ChunkID: randomID, X: r.MapEndX} r.ActiveChunks = append(r.ActiveChunks, newChunk) r.MapEndX += float64(chunkDef.Width * config.TileSize) } else { // Fallback, falls keine Chunks da sind r.MapEndX += 1280 } } // --- NETZWERK --- func (r *Room) Broadcast() { r.Mutex.RLock() defer r.Mutex.RUnlock() state := game.GameState{ RoomID: r.ID, Players: make(map[string]game.PlayerState), Status: r.Status, TimeLeft: r.Countdown, WorldChunks: r.ActiveChunks, HostID: r.HostID, ScrollX: r.GlobalScrollX, } for id, p := range r.Players { state.Players[id] = game.PlayerState{ ID: id, Name: p.Name, X: p.X, Y: p.Y, OnGround: p.OnGround, } } // DEBUG: Ersten Broadcast loggen if len(r.Players) > 0 { log.Printf("📡 Broadcast: Room=%s, Players=%d, Chunks=%d, Status=%s", r.ID, len(state.Players), len(state.WorldChunks), r.Status) } // Senden an "game.update" (Client filtert nicht wirklich, aber für Demo ok) // Besser wäre "game.update." ec, _ := nats.NewEncodedConn(r.NC, nats.JSON_ENCODER) ec.Publish("game.update", state) } // RemovePlayer entfernt einen Spieler aus dem Raum func (r *Room) RemovePlayer(id string) { r.Mutex.Lock() defer r.Mutex.Unlock() delete(r.Players, id) log.Printf("➖ Player %s left room %s", id, r.ID) }