Implement core game functionalities: client prediction, coin collection, scoring, game state synchronization, and player management.
This commit is contained in:
@@ -12,28 +12,34 @@ import (
|
||||
)
|
||||
|
||||
type ServerPlayer struct {
|
||||
ID string
|
||||
Name string
|
||||
X, Y float64
|
||||
VX, VY float64
|
||||
OnGround bool
|
||||
InputX float64 // -1 (Links), 0, 1 (Rechts)
|
||||
ID string
|
||||
Name string
|
||||
X, Y float64
|
||||
VX, VY float64
|
||||
OnGround bool
|
||||
InputX float64 // -1 (Links), 0, 1 (Rechts)
|
||||
LastInputSeq uint32 // Letzte verarbeitete Input-Sequenz
|
||||
Score int
|
||||
IsAlive bool
|
||||
IsSpectator bool
|
||||
}
|
||||
|
||||
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
|
||||
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"
|
||||
ScoreAccum float64 // Akkumulator für Distanz-Score
|
||||
|
||||
stopChan chan struct{}
|
||||
|
||||
@@ -48,13 +54,14 @@ type Room struct {
|
||||
// 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
|
||||
ID: id,
|
||||
NC: nc,
|
||||
World: w,
|
||||
Players: make(map[string]*ServerPlayer),
|
||||
Status: "LOBBY",
|
||||
stopChan: make(chan struct{}),
|
||||
CollectedCoins: make(map[string]bool),
|
||||
pW: 40, pH: 60, // Fallback
|
||||
}
|
||||
|
||||
// Player Werte aus Manifest laden
|
||||
@@ -72,11 +79,22 @@ func NewRoom(id string, nc *nats.Conn, w *game.World) *Room {
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -108,18 +126,27 @@ func (r *Room) AddPlayer(id, name string) {
|
||||
return
|
||||
}
|
||||
|
||||
// Spawn-Position berechnen
|
||||
playerIndex := len(r.Players)
|
||||
spawnX := 100.0 + float64(playerIndex*150)
|
||||
|
||||
p := &ServerPlayer{
|
||||
ID: id,
|
||||
Name: name,
|
||||
X: 100,
|
||||
Y: 200,
|
||||
OnGround: false,
|
||||
ID: id,
|
||||
Name: name,
|
||||
X: spawnX,
|
||||
Y: 200,
|
||||
OnGround: false,
|
||||
Score: 0,
|
||||
IsAlive: true,
|
||||
IsSpectator: false,
|
||||
}
|
||||
|
||||
// Falls das Spiel schon läuft, spawnen wir weiter rechts
|
||||
if r.Status == "RUNNING" {
|
||||
p.X = r.GlobalScrollX + 200
|
||||
p.Y = 200
|
||||
// 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
|
||||
@@ -127,6 +154,21 @@ func (r *Room) AddPlayer(id, name string) {
|
||||
// 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()
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +192,11 @@ func (r *Room) HandleInput(input game.ClientInput) {
|
||||
return
|
||||
}
|
||||
|
||||
// Sequenznummer aktualisieren
|
||||
if input.Sequence > p.LastInputSeq {
|
||||
p.LastInputSeq = input.Sequence
|
||||
}
|
||||
|
||||
switch input.Type {
|
||||
case "JUMP":
|
||||
if p.OnGround {
|
||||
@@ -203,6 +250,11 @@ func (r *Room) Update() {
|
||||
|
||||
// 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
|
||||
@@ -217,6 +269,13 @@ func (r *Room) Update() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prüfe aktuelle Position auf Obstacles (falls reingesprungen)
|
||||
currentHit, currentType := r.CheckCollision(p.X+r.pDrawOffX+r.pHitboxOffX, p.Y+r.pDrawOffY+r.pHitboxOffY, r.pW, r.pH)
|
||||
if currentHit && currentType == "obstacle" {
|
||||
r.KillPlayer(p)
|
||||
continue
|
||||
}
|
||||
|
||||
// X Bewegung
|
||||
currentSpeed := config.RunSpeed + (p.InputX * 4.0)
|
||||
nextX := p.X + currentSpeed
|
||||
@@ -224,7 +283,7 @@ func (r *Room) Update() {
|
||||
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)
|
||||
r.KillPlayer(p)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
@@ -236,7 +295,7 @@ func (r *Room) Update() {
|
||||
p.X = r.GlobalScrollX + 1200
|
||||
} // Rechts Block
|
||||
if p.X < r.GlobalScrollX-50 {
|
||||
r.ResetPlayer(p)
|
||||
r.KillPlayer(p)
|
||||
continue
|
||||
} // Links Tod
|
||||
|
||||
@@ -254,7 +313,7 @@ func (r *Room) Update() {
|
||||
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)
|
||||
r.KillPlayer(p)
|
||||
continue
|
||||
}
|
||||
if p.VY > 0 {
|
||||
@@ -267,8 +326,16 @@ func (r *Room) Update() {
|
||||
}
|
||||
|
||||
if p.Y > 1000 {
|
||||
r.ResetPlayer(p)
|
||||
r.KillPlayer(p)
|
||||
}
|
||||
|
||||
// Coin Kollision prüfen
|
||||
r.CheckCoinCollision(p)
|
||||
}
|
||||
|
||||
// 2b. Distanz-Score updaten
|
||||
if r.Status == "RUNNING" {
|
||||
r.UpdateDistanceScore()
|
||||
}
|
||||
|
||||
// 3. Map Management
|
||||
@@ -289,6 +356,11 @@ 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) {
|
||||
if c.Type == "obstacle" {
|
||||
log.Printf("🔴 OBSTACLE HIT! Player: (%.0f, %.0f, %.0f, %.0f), Obstacle: (%.0f, %.0f, %.0f, %.0f)",
|
||||
playerRect.OffsetX, playerRect.OffsetY, playerRect.W, playerRect.H,
|
||||
c.Rect.OffsetX, c.Rect.OffsetY, c.Rect.W, c.Rect.H)
|
||||
}
|
||||
return true, c.Type
|
||||
}
|
||||
}
|
||||
@@ -346,30 +418,41 @@ func (r *Room) Broadcast() {
|
||||
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,
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
ID: id,
|
||||
Name: p.Name,
|
||||
X: p.X,
|
||||
Y: p.Y,
|
||||
VX: p.VX,
|
||||
VY: p.VY,
|
||||
OnGround: p.OnGround,
|
||||
LastInputSeq: p.LastInputSeq,
|
||||
Score: p.Score,
|
||||
IsAlive: p.IsAlive,
|
||||
IsSpectator: p.IsSpectator,
|
||||
}
|
||||
}
|
||||
|
||||
// DEBUG: Ersten Broadcast loggen
|
||||
if len(r.Players) > 0 {
|
||||
// DEBUG: Ersten Broadcast loggen (nur beim ersten Mal)
|
||||
if 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)
|
||||
}
|
||||
|
||||
// Senden an "game.update" (Client filtert nicht wirklich, aber für Demo ok)
|
||||
// Besser wäre "game.update.<ROOMID>"
|
||||
// Senden an raum-spezifischen Channel: "game.update.<ROOMID>"
|
||||
channel := "game.update." + r.ID
|
||||
ec, _ := nats.NewEncodedConn(r.NC, nats.JSON_ENCODER)
|
||||
ec.Publish("game.update", state)
|
||||
ec.Publish(channel, state)
|
||||
}
|
||||
|
||||
// RemovePlayer entfernt einen Spieler aus dem Raum
|
||||
|
||||
129
pkg/server/scoring.go
Normal file
129
pkg/server/scoring.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game"
|
||||
)
|
||||
|
||||
// CheckCoinCollision prüft ob Spieler Coins einsammelt
|
||||
func (r *Room) CheckCoinCollision(p *ServerPlayer) {
|
||||
if !p.IsAlive || p.IsSpectator {
|
||||
return
|
||||
}
|
||||
|
||||
playerHitbox := game.Rect{
|
||||
OffsetX: p.X + r.pDrawOffX + r.pHitboxOffX,
|
||||
OffsetY: p.Y + r.pDrawOffY + r.pHitboxOffY,
|
||||
W: r.pW,
|
||||
H: r.pH,
|
||||
}
|
||||
|
||||
// Durch alle aktiven Chunks iterieren
|
||||
for _, activeChunk := range r.ActiveChunks {
|
||||
chunkDef, exists := r.World.ChunkLibrary[activeChunk.ChunkID]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
|
||||
// Durch alle Objekte im Chunk
|
||||
for objIdx, obj := range chunkDef.Objects {
|
||||
assetDef, ok := r.World.Manifest.Assets[obj.AssetID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Nur Coins prüfen
|
||||
if assetDef.Type != "coin" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Eindeutiger Key für diesen Coin
|
||||
coinKey := fmt.Sprintf("%s_%d", activeChunk.ChunkID, objIdx)
|
||||
|
||||
// Wurde bereits eingesammelt?
|
||||
if r.CollectedCoins[coinKey] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Coin-Hitbox
|
||||
coinHitbox := game.Rect{
|
||||
OffsetX: activeChunk.X + obj.X + assetDef.Hitbox.OffsetX,
|
||||
OffsetY: obj.Y + assetDef.Hitbox.OffsetY,
|
||||
W: assetDef.Hitbox.W,
|
||||
H: assetDef.Hitbox.H,
|
||||
}
|
||||
|
||||
// Kollision?
|
||||
if game.CheckRectCollision(playerHitbox, coinHitbox) {
|
||||
// Coin einsammeln!
|
||||
r.CollectedCoins[coinKey] = true
|
||||
p.Score += 200
|
||||
log.Printf("💰 %s hat Coin eingesammelt! Score: %d", p.Name, p.Score)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDistanceScore aktualisiert Distanz-basierte Punkte
|
||||
func (r *Room) UpdateDistanceScore() {
|
||||
if r.Status != "RUNNING" {
|
||||
return
|
||||
}
|
||||
|
||||
// Anzahl lebender Spieler zählen
|
||||
aliveCount := 0
|
||||
for _, p := range r.Players {
|
||||
if p.IsAlive && !p.IsSpectator {
|
||||
aliveCount++
|
||||
}
|
||||
}
|
||||
|
||||
if aliveCount == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Multiplier = Anzahl lebender Spieler
|
||||
multiplier := float64(aliveCount)
|
||||
|
||||
// Akkumulator erhöhen: multiplier Punkte pro Sekunde
|
||||
// Bei 60 FPS: multiplier / 60.0 Punkte pro Tick
|
||||
r.ScoreAccum += multiplier / 60.0
|
||||
|
||||
// Wenn Akkumulator >= 1.0, Punkte vergeben
|
||||
if r.ScoreAccum >= 1.0 {
|
||||
pointsToAdd := int(r.ScoreAccum)
|
||||
r.ScoreAccum -= float64(pointsToAdd)
|
||||
|
||||
for _, p := range r.Players {
|
||||
if p.IsAlive && !p.IsSpectator {
|
||||
p.Score += pointsToAdd
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KillPlayer markiert Spieler als tot
|
||||
func (r *Room) KillPlayer(p *ServerPlayer) {
|
||||
if !p.IsAlive {
|
||||
return
|
||||
}
|
||||
|
||||
p.IsAlive = false
|
||||
p.IsSpectator = true
|
||||
log.Printf("💀 %s ist gestorben! Final Score: %d", p.Name, p.Score)
|
||||
|
||||
// Prüfen ob alle tot sind
|
||||
aliveCount := 0
|
||||
for _, pl := range r.Players {
|
||||
if pl.IsAlive && !pl.IsSpectator {
|
||||
aliveCount++
|
||||
}
|
||||
}
|
||||
|
||||
if aliveCount == 0 && r.Status == "RUNNING" {
|
||||
log.Printf("🏁 Alle Spieler tot - Game Over!")
|
||||
r.Status = "GAMEOVER"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user