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

847 lines
22 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"
"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
OnWall bool // Ist an einer Wand
OnMovingPlatform *MovingPlatform // Referenz zur Plattform auf der der Spieler steht
InputX float64 // -1 (Links), 0, 1 (Rechts)
LastInputSeq uint32 // Letzte verarbeitete Input-Sequenz
Score int
IsAlive bool
IsSpectator bool
// Powerups
HasDoubleJump bool // Doppelsprung aktiv?
DoubleJumpUsed bool // Wurde zweiter Sprung schon benutzt?
HasGodMode bool // Godmode aktiv?
GodModeEndTime time.Time // Wann endet Godmode?
}
type MovingPlatform struct {
ChunkID string // Welcher Chunk
ObjectIdx int // Index im Chunk
AssetID string // Asset-ID
CurrentX float64 // Aktuelle Position X (Welt-Koordinaten)
CurrentY float64 // Aktuelle Position Y
StartX float64 // Start-Position X (Welt-Koordinaten)
StartY float64 // Start-Position Y
EndX float64 // End-Position X (Welt-Koordinaten)
EndY float64 // End-Position Y
Speed float64 // Geschwindigkeit
Direction float64 // 1.0 = zu End, -1.0 = zu Start
IsActive bool // Hat die Bewegung bereits begonnen?
HitboxW float64 // Cached Hitbox
HitboxH float64
DrawOffX float64
DrawOffY float64
HitboxOffX float64
HitboxOffY float64
}
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
CollectedCoins map[string]bool // Key: "chunkID_objectIndex"
CollectedPowerups map[string]bool // Key: "chunkID_objectIndex"
ScoreAccum float64 // Akkumulator für Distanz-Score
// Chunk-Pool für fairen Random-Spawn
ChunkPool []string // Verfügbare Chunks für nächsten Spawn
ChunkSpawnedCount map[string]int // Wie oft wurde jeder Chunk gespawnt
MovingPlatforms []*MovingPlatform // Aktive bewegende Plattformen
firstBroadcast bool // Wurde bereits geloggt?
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{}),
CollectedCoins: make(map[string]bool),
CollectedPowerups: make(map[string]bool),
ChunkSpawnedCount: make(map[string]int),
pW: 40, pH: 60, // Fallback
}
// Initialisiere Chunk-Pool mit allen verfügbaren Chunks
r.RefillChunkPool()
// 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
// DEBUG: Verfügbare Chunks anzeigen
log.Printf("🗂️ Raum %s: ChunkLibrary hat %d Einträge", id, len(w.ChunkLibrary))
for cid := range w.ChunkLibrary {
log.Printf(" - %s", cid)
}
// Erste Chunks generieren
r.SpawnNextChunk()
r.SpawnNextChunk()
r.Colliders = r.World.GenerateColliders(r.ActiveChunks)
// DEBUG: Collider-Typen zählen
obstacleCount := 0
platformCount := 0
for _, c := range r.Colliders {
if c.Type == "obstacle" {
obstacleCount++
} else if c.Type == "platform" {
platformCount++
}
}
log.Printf("🔍 Initial Colliders: %d total (%d obstacles, %d platforms)", len(r.Colliders), obstacleCount, platformCount)
log.Printf("🎬 Raum %s gestartet mit %d Chunks", id, len(r.ActiveChunks))
for _, ac := range r.ActiveChunks {
log.Printf(" - Chunk '%s' bei X=%.0f", ac.ChunkID, ac.X)
}
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
}
// Spawn-Position berechnen
playerIndex := len(r.Players)
spawnX := 100.0 + float64(playerIndex*150)
p := &ServerPlayer{
ID: id,
Name: name,
X: spawnX,
Y: 200,
OnGround: false,
Score: 0,
IsAlive: true,
IsSpectator: false,
}
// Falls das Spiel schon läuft, NICHT joinen erlauben
if r.Status == "RUNNING" || r.Status == "COUNTDOWN" {
// Spieler wird als Zuschauer hinzugefügt
p.IsSpectator = true
p.IsAlive = false
log.Printf("⚠️ Spieler %s joined während des Spiels - wird Zuschauer", name)
}
r.Players[id] = p
// Erster Spieler wird Host
if r.HostID == "" {
r.HostID = id
// Auto-Start nur für Solo-Räume (erkennen am "solo_" Prefix)
if len(r.ID) > 5 && r.ID[:5] == "solo_" {
log.Printf("⏰ Solo-Mode: Auto-Start in 2 Sekunden für Raum %s", r.ID)
go func() {
time.Sleep(2 * time.Second)
r.Mutex.Lock()
if r.Status == "LOBBY" {
r.Status = "COUNTDOWN"
r.NextStart = time.Now().Add(3 * time.Second)
log.Printf("🎮 Raum %s: Countdown gestartet!", r.ID)
}
r.Mutex.Unlock()
}()
}
}
}
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
}
// Sequenznummer aktualisieren
if input.Sequence > p.LastInputSeq {
p.LastInputSeq = input.Sequence
}
switch input.Type {
case "JUMP":
if p.OnGround {
p.VY = -14.0
p.OnGround = false
p.DoubleJumpUsed = false // Reset double jump on ground jump
} else if p.HasDoubleJump && !p.DoubleJumpUsed {
// Double Jump in der Luft
p.VY = -14.0
p.DoubleJumpUsed = true
log.Printf("⚡ %s verwendet Double Jump!", p.Name)
}
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
// Bewegende Plattformen updaten
r.UpdateMovingPlatforms()
}
maxX := r.GlobalScrollX
// 2. Spieler Physik
for _, p := range r.Players {
// Skip tote/Zuschauer Spieler
if !p.IsAlive || p.IsSpectator {
continue
}
// 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
// Symmetrische Geschwindigkeit: Links = Rechts
// Nach rechts: RunSpeed + 11, Nach links: RunSpeed - 11
// Ergebnis: Rechts = 18, Links = -4 (beide gleich weit vom Scroll)
currentSpeed := config.RunSpeed + (p.InputX * 11.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 == "wall" {
// Wand getroffen - kann klettern!
p.OnWall = true
// X-Position nicht ändern (bleibt an der Wand)
} else if typeX == "obstacle" {
// Godmode prüfen
if p.HasGodMode && time.Now().Before(p.GodModeEndTime) {
// Mit Godmode - Obstacle wird zerstört, Spieler überlebt
p.X = nextX
// TODO: Obstacle aus colliders entfernen (benötigt Referenz zum Obstacle)
log.Printf("🛡️ %s zerstört Obstacle mit Godmode!", p.Name)
} else {
// Ohne Godmode - Spieler stirbt
p.X = nextX
r.KillPlayer(p)
continue
}
} else {
// Platform blockiert
p.OnWall = false
}
} else {
p.X = nextX
p.OnWall = false
}
// Grenzen
if p.X > r.GlobalScrollX+1200 {
p.X = r.GlobalScrollX + 1200
} // Rechts Block
if p.X < r.GlobalScrollX-50 {
r.KillPlayer(p)
continue
} // Links Tod
if p.X > maxX {
maxX = p.X
}
// Y Bewegung
// An der Wand: Reduzierte Gravität + Klettern mit InputX
if p.OnWall {
// Wandrutschen (langsame Fallgeschwindigkeit)
p.VY += config.Gravity * 0.3 // 30% Gravität an der Wand
if p.VY > 3.0 {
p.VY = 3.0 // Maximal 3.0 beim Rutschen
}
// Hochklettern wenn nach oben gedrückt (InputX in Wandrichtung)
if p.InputX != 0 {
p.VY = -5.0 // Kletter-Geschwindigkeit nach oben
}
} else {
// Normal: Volle Gravität
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 == "wall" {
// An der Wand: Nicht töten, sondern Position halten
if p.OnWall {
p.VY = 0
} else {
// Von oben/unten gegen Wand - töten (kein Klettern in Y-Richtung)
p.Y = nextY
r.KillPlayer(p)
continue
}
} else if typeY == "obstacle" {
// Obstacle - immer töten
p.Y = nextY
r.KillPlayer(p)
continue
} else {
// Platform blockiert
if p.VY > 0 {
p.OnGround = true
// Prüfe ob auf bewegender Plattform
platform := r.CheckMovingPlatformLanding(p.X+r.pDrawOffX+r.pHitboxOffX, nextY+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH)
p.OnMovingPlatform = platform
}
p.VY = 0
}
} else {
p.Y += p.VY
p.OnGround = false
p.OnMovingPlatform = nil
}
// Spieler bewegt sich mit Plattform mit
if p.OnMovingPlatform != nil && p.OnGround {
// Berechne Plattform-Geschwindigkeit
mp := p.OnMovingPlatform
var targetX, targetY float64
if mp.Direction > 0 {
targetX, targetY = mp.EndX, mp.EndY
} else {
targetX, targetY = mp.StartX, mp.StartY
}
dx := targetX - mp.CurrentX
dy := targetY - mp.CurrentY
dist := math.Sqrt(dx*dx + dy*dy)
if dist > 0.1 {
movePerTick := mp.Speed / 60.0
platformVelX := (dx / dist) * movePerTick
platformVelY := (dy / dist) * movePerTick
// Übertrage Plattform-Geschwindigkeit auf Spieler
p.X += platformVelX
p.Y += platformVelY
}
}
if p.Y > 1000 {
r.KillPlayer(p)
}
// Coin Kollision prüfen
r.CheckCoinCollision(p)
// Powerup Kollision prüfen
r.CheckPowerupCollision(p)
// Godmode Timeout prüfen
if p.HasGodMode && time.Now().After(p.GodModeEndTime) {
p.HasGodMode = false
log.Printf("🛡️ Godmode von %s ist abgelaufen", p.Name)
}
}
// 2b. Distanz-Score updaten
if r.Status == "RUNNING" {
r.UpdateDistanceScore()
}
// 3. Map Management
r.UpdateMapLogic(maxX)
// 4. Prüfen ob alle Spieler tot sind (GAMEOVER Check)
if r.Status == "RUNNING" {
aliveCount := 0
for _, p := range r.Players {
if p.IsAlive && !p.IsSpectator {
aliveCount++
}
}
if aliveCount == 0 && len(r.Players) > 0 {
log.Printf("🏁 Alle Spieler tot - Game Over!")
r.Status = "GAMEOVER"
}
}
// 5. 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}
// 1. Statische Colliders (Chunks)
for _, c := range r.Colliders {
if game.CheckRectCollision(playerRect, c.Rect) {
log.Printf("🔴 COLLISION! Type=%s, Player: (%.1f, %.1f, %.1f x %.1f), Collider: (%.1f, %.1f, %.1f x %.1f)",
c.Type,
playerRect.OffsetX, playerRect.OffsetY, playerRect.W, playerRect.H,
c.Rect.OffsetX, c.Rect.OffsetY, c.Rect.W, c.Rect.H)
return true, c.Type
}
}
// 2. Bewegende Plattformen (dynamische Colliders)
for _, mp := range r.MovingPlatforms {
// Berechne Plattform-Hitbox an aktueller Position
mpRect := game.Rect{
OffsetX: mp.CurrentX + mp.DrawOffX + mp.HitboxOffX,
OffsetY: mp.CurrentY + mp.DrawOffY + mp.HitboxOffY,
W: mp.HitboxW,
H: mp.HitboxH,
}
if game.CheckRectCollision(playerRect, mpRect) {
return true, "platform"
}
}
return false, ""
}
// CheckMovingPlatformLanding prüft ob Spieler auf einer bewegenden Plattform landet
func (r *Room) CheckMovingPlatformLanding(x, y, w, h float64) *MovingPlatform {
playerRect := game.Rect{OffsetX: x, OffsetY: y, W: w, H: h}
for _, mp := range r.MovingPlatforms {
mpRect := game.Rect{
OffsetX: mp.CurrentX + mp.DrawOffX + mp.HitboxOffX,
OffsetY: mp.CurrentY + mp.DrawOffY + mp.HitboxOffY,
W: mp.HitboxW,
H: mp.HitboxH,
}
if game.CheckRectCollision(playerRect, mpRect) {
return mp
}
}
return nil
}
func (r *Room) UpdateMapLogic(maxX float64) {
if r.Status != "RUNNING" {
return
}
// Neue Chunks spawnen
if maxX > r.MapEndX-2000 {
r.SpawnNextChunk()
oldCount := len(r.Colliders)
r.Colliders = r.World.GenerateColliders(r.ActiveChunks)
log.Printf("🆕 Chunk gespawnt - Colliders: %d -> %d", oldCount, len(r.Colliders))
}
// 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 {
// Lösche alle Coins dieses Chunks aus CollectedCoins
r.ClearChunkCoins(firstChunk.ChunkID)
// Lösche alle Powerups dieses Chunks
r.ClearChunkPowerups(firstChunk.ChunkID)
// Entferne bewegende Plattformen dieses Chunks
r.RemoveMovingPlatforms(firstChunk.ChunkID)
r.ActiveChunks = r.ActiveChunks[1:]
r.Colliders = r.World.GenerateColliders(r.ActiveChunks)
log.Printf("🗑️ Chunk despawned: %s", firstChunk.ChunkID)
}
}
}
// ClearChunkCoins löscht alle eingesammelten Coins eines Chunks
func (r *Room) ClearChunkCoins(chunkID string) {
prefix := chunkID + "_"
coinsCleared := 0
for key := range r.CollectedCoins {
if len(key) >= len(prefix) && key[:len(prefix)] == prefix {
delete(r.CollectedCoins, key)
coinsCleared++
}
}
if coinsCleared > 0 {
log.Printf("💰 %d Coins von Chunk %s zurückgesetzt", coinsCleared, chunkID)
}
}
// InitMovingPlatforms initialisiert bewegende Plattformen für einen Chunk
func (r *Room) InitMovingPlatforms(chunkID string, chunkWorldX float64) {
chunkDef, exists := r.World.ChunkLibrary[chunkID]
if !exists {
return
}
for objIdx, obj := range chunkDef.Objects {
if obj.MovingPlatform != nil {
assetDef, ok := r.World.Manifest.Assets[obj.AssetID]
if !ok || assetDef.Type != "platform" {
continue
}
mpData := obj.MovingPlatform
platform := &MovingPlatform{
ChunkID: chunkID,
ObjectIdx: objIdx,
AssetID: obj.AssetID,
StartX: chunkWorldX + mpData.StartX,
StartY: mpData.StartY,
EndX: chunkWorldX + mpData.EndX,
EndY: mpData.EndY,
Speed: mpData.Speed,
Direction: 1.0, // Start bei StartX, bewege zu EndX
HitboxW: assetDef.Hitbox.W,
HitboxH: assetDef.Hitbox.H,
DrawOffX: assetDef.DrawOffX,
DrawOffY: assetDef.DrawOffY,
HitboxOffX: assetDef.Hitbox.OffsetX,
HitboxOffY: assetDef.Hitbox.OffsetY,
}
platform.CurrentX = platform.StartX
platform.CurrentY = platform.StartY
r.MovingPlatforms = append(r.MovingPlatforms, platform)
log.Printf("🔄 Bewegende Plattform initialisiert: %s in Chunk %s", obj.AssetID, chunkID)
}
}
}
// RemoveMovingPlatforms entfernt alle Plattformen eines Chunks
func (r *Room) RemoveMovingPlatforms(chunkID string) {
newPlatforms := make([]*MovingPlatform, 0)
removedCount := 0
for _, p := range r.MovingPlatforms {
if p.ChunkID != chunkID {
newPlatforms = append(newPlatforms, p)
} else {
removedCount++
}
}
r.MovingPlatforms = newPlatforms
if removedCount > 0 {
log.Printf("🗑️ %d bewegende Plattformen von Chunk %s entfernt", removedCount, chunkID)
}
}
// UpdateMovingPlatforms bewegt alle aktiven Plattformen
func (r *Room) UpdateMovingPlatforms() {
// Sichtbarer Bereich: GlobalScrollX bis GlobalScrollX + 1400
// Aktivierung bei 3/4: GlobalScrollX + (1400 * 3/4) = GlobalScrollX + 1050
activationPoint := r.GlobalScrollX + 1050
for _, p := range r.MovingPlatforms {
// Prüfe ob Plattform den Aktivierungspunkt erreicht hat
if !p.IsActive {
// Aktiviere Plattform, wenn sie bei 3/4 des Bildschirms ist
if p.CurrentX <= activationPoint {
p.IsActive = true
log.Printf("▶️ Plattform aktiviert: %s (X=%.0f)", p.ChunkID, p.CurrentX)
} else {
// Noch nicht weit genug gescrollt, nicht bewegen
continue
}
}
// Bewegung berechnen (Speed pro Sekunde, bei 60 FPS = Speed/60)
movePerTick := p.Speed / 60.0
// Bewegungsvektor von CurrentPos zu Ziel
var targetX, targetY float64
if p.Direction > 0 {
targetX, targetY = p.EndX, p.EndY
} else {
targetX, targetY = p.StartX, p.StartY
}
dx := targetX - p.CurrentX
dy := targetY - p.CurrentY
dist := math.Sqrt(dx*dx + dy*dy)
if dist < movePerTick {
// Ziel erreicht, umkehren
p.CurrentX = targetX
p.CurrentY = targetY
p.Direction *= -1.0
} else {
// Weiterbewegen
p.CurrentX += (dx / dist) * movePerTick
p.CurrentY += (dy / dist) * movePerTick
}
}
}
// RefillChunkPool füllt den Pool mit allen verfügbaren Chunks
func (r *Room) RefillChunkPool() {
r.ChunkPool = make([]string, 0, len(r.World.ChunkLibrary))
for chunkID := range r.World.ChunkLibrary {
if chunkID != "start" { // Start-Chunk nicht in Pool
r.ChunkPool = append(r.ChunkPool, chunkID)
}
}
// Mische Pool für zufällige Reihenfolge
rand.Shuffle(len(r.ChunkPool), func(i, j int) {
r.ChunkPool[i], r.ChunkPool[j] = r.ChunkPool[j], r.ChunkPool[i]
})
log.Printf("🔄 Chunk-Pool neu gefüllt: %d Chunks", len(r.ChunkPool))
}
func (r *Room) SpawnNextChunk() {
// Pool leer? Nachfüllen!
if len(r.ChunkPool) == 0 {
r.RefillChunkPool()
}
if len(r.ChunkPool) > 0 {
// Nimm ersten Chunk aus Pool (bereits gemischt)
randomID := r.ChunkPool[0]
r.ChunkPool = r.ChunkPool[1:] // Entferne aus Pool
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)
// Initialisiere bewegende Plattformen für diesen Chunk
r.InitMovingPlatforms(randomID, newChunk.X)
r.ChunkSpawnedCount[randomID]++
log.Printf("🎲 Chunk gespawnt: %s (Total: %d mal, Pool: %d übrig)", randomID, r.ChunkSpawnedCount[randomID], len(r.ChunkPool))
} 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,
CollectedCoins: r.CollectedCoins,
CollectedPowerups: r.CollectedPowerups,
MovingPlatforms: make([]game.MovingPlatformSync, 0, len(r.MovingPlatforms)),
}
for id, p := range r.Players {
state.Players[id] = game.PlayerState{
ID: id,
Name: p.Name,
X: p.X,
Y: p.Y,
VX: p.VX,
VY: p.VY,
OnGround: p.OnGround,
OnWall: p.OnWall,
LastInputSeq: p.LastInputSeq,
Score: p.Score,
IsAlive: p.IsAlive,
IsSpectator: p.IsSpectator,
HasDoubleJump: p.HasDoubleJump,
HasGodMode: p.HasGodMode,
}
}
// Bewegende Plattformen synchronisieren
for _, mp := range r.MovingPlatforms {
state.MovingPlatforms = append(state.MovingPlatforms, game.MovingPlatformSync{
ChunkID: mp.ChunkID,
ObjectIdx: mp.ObjectIdx,
AssetID: mp.AssetID,
X: mp.CurrentX,
Y: mp.CurrentY,
})
}
// DEBUG: Ersten Broadcast loggen (nur beim ersten Mal)
if !r.firstBroadcast && len(r.Players) > 0 && r.Status == "LOBBY" {
log.Printf("📡 Broadcast: Room=%s, Players=%d, Chunks=%d, Status=%s", r.ID, len(state.Players), len(state.WorldChunks), r.Status)
r.firstBroadcast = true
}
// Senden an raum-spezifischen Channel: "game.update.<ROOMID>"
channel := "game.update." + r.ID
ec, _ := nats.NewEncodedConn(r.NC, nats.JSON_ENCODER)
ec.Publish(channel, 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)
}
// ClearChunkPowerups löscht alle eingesammelten Powerups eines Chunks
func (r *Room) ClearChunkPowerups(chunkID string) {
prefix := chunkID + "_"
powerupsCleared := 0
for key := range r.CollectedPowerups {
if len(key) >= len(prefix) && key[:len(prefix)] == prefix {
delete(r.CollectedPowerups, key)
powerupsCleared++
}
}
if powerupsCleared > 0 {
log.Printf("⚡ %d Powerups von Chunk %s zurückgesetzt", powerupsCleared, chunkID)
}
}