Private
Public Access
1
0
Files
EscapeFromTeacher/pkg/server/room.go

382 lines
7.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.<ROOMID>"
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)
}