Add platform-specific implementations for assets, audio, WebSocket, and rendering on Desktop and WebAssembly platforms. Introduce embedded assets for WebAssembly and native file handling for Desktop. Add platform-specific chunk loading and game state synchronization.
This commit is contained in:
@@ -40,48 +40,42 @@ func InitLeaderboard(redisAddr string) error {
|
||||
}
|
||||
|
||||
func (lb *Leaderboard) AddScore(name, code string, score int) bool {
|
||||
// Prüfe ob Spieler bereits existiert
|
||||
existingScoreStr, err := lb.rdb.HGet(lb.ctx, "leaderboard:players", code).Result()
|
||||
if err == nil {
|
||||
var existingScore int
|
||||
json.Unmarshal([]byte(existingScoreStr), &existingScore)
|
||||
if score <= existingScore {
|
||||
return false // Neuer Score nicht besser
|
||||
}
|
||||
}
|
||||
// Erstelle eindeutigen Key für diesen Score: PlayerCode + Timestamp
|
||||
timestamp := time.Now().Unix()
|
||||
uniqueKey := code + "_" + time.Now().Format("20060102_150405")
|
||||
|
||||
// Score speichern
|
||||
entry := game.LeaderboardEntry{
|
||||
PlayerName: name,
|
||||
PlayerCode: code,
|
||||
Score: score,
|
||||
Timestamp: time.Now().Unix(),
|
||||
Timestamp: timestamp,
|
||||
}
|
||||
|
||||
data, _ := json.Marshal(entry)
|
||||
lb.rdb.HSet(lb.ctx, "leaderboard:players", code, string(data))
|
||||
lb.rdb.HSet(lb.ctx, "leaderboard:entries", uniqueKey, string(data))
|
||||
|
||||
// In Sorted Set mit Score als Wert
|
||||
// In Sorted Set mit Score als Wert (uniqueKey statt code!)
|
||||
lb.rdb.ZAdd(lb.ctx, leaderboardKey, redis.Z{
|
||||
Score: float64(score),
|
||||
Member: code,
|
||||
Member: uniqueKey,
|
||||
})
|
||||
|
||||
log.Printf("🏆 Leaderboard Update: %s mit %d Punkten", name, score)
|
||||
log.Printf("🏆 Leaderboard: %s mit %d Punkten (Entry: %s)", name, score, uniqueKey)
|
||||
return true
|
||||
}
|
||||
|
||||
func (lb *Leaderboard) GetTop10() []game.LeaderboardEntry {
|
||||
// Hole Top 10 (höchste Scores zuerst)
|
||||
codes, err := lb.rdb.ZRevRange(lb.ctx, leaderboardKey, 0, 9).Result()
|
||||
uniqueKeys, err := lb.rdb.ZRevRange(lb.ctx, leaderboardKey, 0, 9).Result()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Fehler beim Abrufen des Leaderboards: %v", err)
|
||||
return []game.LeaderboardEntry{}
|
||||
}
|
||||
|
||||
entries := make([]game.LeaderboardEntry, 0)
|
||||
for _, code := range codes {
|
||||
dataStr, err := lb.rdb.HGet(lb.ctx, "leaderboard:players", code).Result()
|
||||
for _, uniqueKey := range uniqueKeys {
|
||||
dataStr, err := lb.rdb.HGet(lb.ctx, "leaderboard:entries", uniqueKey).Result()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,34 +13,70 @@ import (
|
||||
)
|
||||
|
||||
type ServerPlayer struct {
|
||||
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
|
||||
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"
|
||||
ScoreAccum float64 // Akkumulator für Distanz-Score
|
||||
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{}
|
||||
|
||||
@@ -54,16 +91,21 @@ 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{}),
|
||||
CollectedCoins: make(map[string]bool),
|
||||
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),
|
||||
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
|
||||
@@ -214,6 +256,12 @@ func (r *Room) HandleInput(input game.ClientInput) {
|
||||
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
|
||||
@@ -256,6 +304,8 @@ func (r *Room) Update() {
|
||||
}
|
||||
} else if r.Status == "RUNNING" {
|
||||
r.GlobalScrollX += config.RunSpeed
|
||||
// Bewegende Plattformen updaten
|
||||
r.UpdateMovingPlatforms()
|
||||
}
|
||||
|
||||
maxX := r.GlobalScrollX
|
||||
@@ -282,20 +332,38 @@ func (r *Room) Update() {
|
||||
}
|
||||
|
||||
// X Bewegung
|
||||
currentSpeed := config.RunSpeed + (p.InputX * 4.0)
|
||||
// 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 == "obstacle" {
|
||||
// Nicht blocken, sondern weiterlaufen und töten
|
||||
p.X = nextX
|
||||
r.KillPlayer(p)
|
||||
continue
|
||||
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
|
||||
}
|
||||
// Platform blockiert
|
||||
} else {
|
||||
p.X = nextX
|
||||
p.OnWall = false
|
||||
}
|
||||
|
||||
// Grenzen
|
||||
@@ -312,28 +380,85 @@ func (r *Room) Update() {
|
||||
}
|
||||
|
||||
// Y Bewegung
|
||||
p.VY += config.Gravity
|
||||
if p.VY > config.MaxFall {
|
||||
p.VY = config.MaxFall
|
||||
// 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 == "obstacle" {
|
||||
// Nicht blocken, sondern weiterlaufen und töten
|
||||
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
|
||||
}
|
||||
// Platform blockiert
|
||||
if p.VY > 0 {
|
||||
p.OnGround = true
|
||||
}
|
||||
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 {
|
||||
@@ -342,6 +467,15 @@ func (r *Room) Update() {
|
||||
|
||||
// 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
|
||||
@@ -380,6 +514,7 @@ func (r *Room) Update() {
|
||||
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)",
|
||||
@@ -390,9 +525,44 @@ func (r *Room) CheckCollision(x, y, w, h float64) (bool, string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -413,26 +583,178 @@ func (r *Room) UpdateMapLogic(maxX float64) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Room) SpawnNextChunk() {
|
||||
keys := make([]string, 0, len(r.World.ChunkLibrary))
|
||||
for k := range r.World.ChunkLibrary {
|
||||
keys = append(keys, k)
|
||||
// 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
|
||||
}
|
||||
|
||||
if len(keys) > 0 {
|
||||
// Zufälligen Chunk wählen
|
||||
randomID := keys[rand.Intn(len(keys))]
|
||||
chunkDef := r.World.ChunkLibrary[randomID]
|
||||
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
|
||||
@@ -446,35 +768,52 @@ 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,
|
||||
CollectedCoins: r.CollectedCoins,
|
||||
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,
|
||||
LastInputSeq: p.LastInputSeq,
|
||||
Score: p.Score,
|
||||
IsAlive: p.IsAlive,
|
||||
IsSpectator: p.IsSpectator,
|
||||
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 len(r.Players) > 0 && r.Status == "LOBBY" {
|
||||
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>"
|
||||
@@ -490,3 +829,18 @@ func (r *Room) RemovePlayer(id string) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game"
|
||||
)
|
||||
@@ -47,10 +48,10 @@ func (r *Room) CheckCoinCollision(p *ServerPlayer) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Coin-Hitbox
|
||||
// Coin-Hitbox (muss DrawOffX/Y einbeziehen wie bei Obstacles!)
|
||||
coinHitbox := game.Rect{
|
||||
OffsetX: activeChunk.X + obj.X + assetDef.Hitbox.OffsetX,
|
||||
OffsetY: obj.Y + assetDef.Hitbox.OffsetY,
|
||||
OffsetX: activeChunk.X + obj.X + assetDef.DrawOffX + assetDef.Hitbox.OffsetX,
|
||||
OffsetY: obj.Y + assetDef.DrawOffY + assetDef.Hitbox.OffsetY,
|
||||
W: assetDef.Hitbox.W,
|
||||
H: assetDef.Hitbox.H,
|
||||
}
|
||||
@@ -66,39 +67,93 @@ func (r *Room) CheckCoinCollision(p *ServerPlayer) {
|
||||
}
|
||||
}
|
||||
|
||||
// CheckPowerupCollision prüft ob Spieler Powerups einsammelt
|
||||
func (r *Room) CheckPowerupCollision(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 Powerups prüfen
|
||||
if assetDef.Type != "powerup" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Eindeutiger Key für dieses Powerup
|
||||
powerupKey := fmt.Sprintf("%s_%d", activeChunk.ChunkID, objIdx)
|
||||
|
||||
// Wurde bereits eingesammelt?
|
||||
if r.CollectedPowerups[powerupKey] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Powerup-Hitbox
|
||||
powerupHitbox := game.Rect{
|
||||
OffsetX: activeChunk.X + obj.X + assetDef.DrawOffX + assetDef.Hitbox.OffsetX,
|
||||
OffsetY: obj.Y + assetDef.DrawOffY + assetDef.Hitbox.OffsetY,
|
||||
W: assetDef.Hitbox.W,
|
||||
H: assetDef.Hitbox.H,
|
||||
}
|
||||
|
||||
// Kollision?
|
||||
if game.CheckRectCollision(playerHitbox, powerupHitbox) {
|
||||
// Powerup einsammeln!
|
||||
r.CollectedPowerups[powerupKey] = true
|
||||
|
||||
// Powerup-Effekt anwenden
|
||||
switch obj.AssetID {
|
||||
case "jumpboost":
|
||||
p.HasDoubleJump = true
|
||||
p.DoubleJumpUsed = false
|
||||
log.Printf("⚡ %s hat Double Jump erhalten!", p.Name)
|
||||
|
||||
case "godmode":
|
||||
p.HasGodMode = true
|
||||
p.GodModeEndTime = time.Now().Add(10 * time.Second)
|
||||
log.Printf("🛡️ %s hat Godmode erhalten! (10 Sekunden)", p.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateDistanceScore aktualisiert Distanz-basierte Punkte
|
||||
func (r *Room) UpdateDistanceScore() {
|
||||
if r.Status != "RUNNING" {
|
||||
return
|
||||
}
|
||||
|
||||
// Anzahl lebender Spieler zählen
|
||||
aliveCount := 0
|
||||
// Jeder Spieler bekommt Punkte basierend auf seiner eigenen Distanz
|
||||
// Punkte = (X-Position / TileSize) = Distanz in Tiles
|
||||
for _, p := range r.Players {
|
||||
if p.IsAlive && !p.IsSpectator {
|
||||
aliveCount++
|
||||
}
|
||||
}
|
||||
// Berechne Score basierend auf X-Position
|
||||
// 1 Punkt pro Tile (64px)
|
||||
newScore := int(p.X / 64.0)
|
||||
|
||||
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
|
||||
// Nur updaten wenn höher als aktueller Score
|
||||
if newScore > p.Score {
|
||||
p.Score = newScore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user