From 656f279a89f21a38efe2fae13fa7436e1f00b55b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Untersch=C3=BCtz?= Date: Sun, 22 Mar 2026 18:18:45 +0100 Subject: [PATCH] fix game --- cmd/client/main.go | 14 ++--- cmd/client/particles.go | 57 ++++++++++++++++---- cmd/client/prediction.go | 16 ++++++ cmd/client/web/assets/assets.json | 18 +++++++ pkg/game/data.go | 35 ++++++------ pkg/server/room.go | 84 ++++++++++++++++++++++------- pkg/server/scoring.go | 89 +++++++++++++++++-------------- 7 files changed, 226 insertions(+), 87 deletions(-) diff --git a/cmd/client/main.go b/cmd/client/main.go index 2383bfc..3bc2dc3 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -82,12 +82,14 @@ type Game struct { lastStatus string // Client Prediction - predictedX float64 // Vorhergesagte Position - predictedY float64 - predictedVX float64 - predictedVY float64 - predictedGround bool - predictedOnWall bool + predictedX float64 // Vorhergesagte Position + predictedY float64 + predictedVX float64 + predictedVY float64 + predictedGround bool + predictedOnWall bool + predictedHasDoubleJump bool // Lokale Kopie des Double-Jump-Powerups + predictedDoubleJumpUsed bool // Wurde zweiter Sprung schon verbraucht? currentSpeed float64 // Aktuelle Scroll-Geschwindigkeit vom Server inputSequence uint32 // Sequenznummer für Inputs pendingInputs map[uint32]InputState // Noch nicht bestätigte Inputs diff --git a/cmd/client/particles.go b/cmd/client/particles.go index 90374dc..678fabe 100644 --- a/cmd/client/particles.go +++ b/cmd/client/particles.go @@ -51,8 +51,13 @@ func (g *Game) SpawnCoinParticles(x, y float64) { } } -// SpawnPowerupAura erstellt Partikel-Aura um Spieler mit aktivem Powerup -func (g *Game) SpawnPowerupAura(x, y float64, powerupType string) { +// SpawnPowerupAura erstellt Partikel-Aura um Spieler mit aktivem Powerup. +// intensity: 0.0 (schwach/auslaufend) bis 1.0 (voll aktiv) – steuert Spawn-Rate und Partikelgröße. +func (g *Game) SpawnPowerupAura(x, y float64, powerupType string, intensity float64) { + if intensity <= 0 { + return + } + g.particlesMutex.Lock() defer g.particlesMutex.Unlock() @@ -63,18 +68,24 @@ func (g *Game) SpawnPowerupAura(x, y float64, powerupType string) { particleColor = color.RGBA{100, 200, 255, 200} // Hellblau case "godmode": particleColor = color.RGBA{255, 215, 0, 200} // Gold + case "magnet": + particleColor = color.RGBA{255, 80, 220, 200} // Pink/Magenta default: particleColor = color.RGBA{200, 100, 255, 200} // Lila } - // Nur gelegentlich spawnen (nicht jedes Frame) - 1 Partikel alle 3-4 Frames - if rand.Intn(3) != 0 { + // Spawn-Rate skaliert mit Intensität: bei voll aktiv ~1/3 Chance, bei fast leer ~1/10 + spawnThreshold := int(3.0/intensity + 0.5) + if spawnThreshold < 1 { + spawnThreshold = 1 + } + if rand.Intn(spawnThreshold) != 0 { return } - // 1 Partikel für sanfte Aura - spawnen in alle Richtungen angle := rand.Float64() * 2 * math.Pi distance := 25.0 + rand.Float64()*20.0 + size := (2.5 + rand.Float64()*1.5) * intensity g.particles = append(g.particles, Particle{ X: x + math.Cos(angle)*distance, @@ -82,8 +93,8 @@ func (g *Game) SpawnPowerupAura(x, y float64, powerupType string) { VX: math.Cos(angle) * 0.5, VY: math.Sin(angle) * 0.5, Life: 1.0, - MaxLife: 1.0 + rand.Float64()*0.5, - Size: 2.5 + rand.Float64()*1.5, + MaxLife: (1.0 + rand.Float64()*0.5) * intensity, + Size: size, Color: particleColor, Type: "powerup", Gravity: false, @@ -299,10 +310,38 @@ func (g *Game) DetectAndSpawnParticles() { centerY := player.Y - 231 + 42 + 184/2 if player.HasDoubleJump { - g.SpawnPowerupAura(centerX, centerY, "doublejump") + // Intensität nimmt mit verbleibender Zeit ab (0..15s → 1.0..0.1) + // Zusätzlich schwächer wenn Sprung bereits verbraucht + intensity := player.DoubleJumpRemainingSeconds / 15.0 + if intensity > 1.0 { + intensity = 1.0 + } else if intensity < 0.1 { + intensity = 0.1 + } + if player.DoubleJumpUsed { + intensity *= 0.4 + } + g.SpawnPowerupAura(centerX, centerY, "doublejump", intensity) } if player.HasGodMode { - g.SpawnPowerupAura(centerX, centerY, "godmode") + // Intensität nimmt mit verbleibender Zeit ab (0..10s → 1.0..0.1) + intensity := player.GodModeRemainingSeconds / 10.0 + if intensity > 1.0 { + intensity = 1.0 + } else if intensity < 0.1 { + intensity = 0.1 + } + g.SpawnPowerupAura(centerX, centerY, "godmode", intensity) + } + if player.HasMagnet { + // Intensität nimmt mit verbleibender Zeit ab (0..8s → 1.0..0.1) + intensity := player.MagnetRemainingSeconds / 8.0 + if intensity > 1.0 { + intensity = 1.0 + } else if intensity < 0.1 { + intensity = 0.1 + } + g.SpawnPowerupAura(centerX, centerY, "magnet", intensity) } } diff --git a/cmd/client/prediction.go b/cmd/client/prediction.go index 3a907f8..1ef0539 100644 --- a/cmd/client/prediction.go +++ b/cmd/client/prediction.go @@ -1,6 +1,7 @@ package main import ( + "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/config" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/physics" ) @@ -31,6 +32,17 @@ func (g *Game) ApplyInput(input InputState) { OnWall: g.predictedOnWall, } + // Double Jump: vor der Physik anwenden (1:1 wie Server) + if input.Jump && !g.predictedGround && g.predictedHasDoubleJump && !g.predictedDoubleJumpUsed { + g.predictedVY = -config.JumpVelocity + g.predictedDoubleJumpUsed = true + } + + // Double Jump Reset wenn wieder am Boden + if g.predictedGround { + g.predictedDoubleJumpUsed = false + } + // Physik-Input vorbereiten physicsInput := physics.PhysicsInput{ InputX: moveX, @@ -175,4 +187,8 @@ func (g *Game) ReconcileWithServer(serverState game.PlayerState) { g.predictedVY = replayVY g.predictedGround = replayGround g.predictedOnWall = replayOnWall + + // Powerup-State vom Server übernehmen (autoritativ) + g.predictedHasDoubleJump = serverState.HasDoubleJump + g.predictedDoubleJumpUsed = serverState.DoubleJumpUsed } diff --git a/cmd/client/web/assets/assets.json b/cmd/client/web/assets/assets.json index 4f4e792..10a17f9 100644 --- a/cmd/client/web/assets/assets.json +++ b/cmd/client/web/assets/assets.json @@ -108,6 +108,24 @@ "Type": "" } }, + "magnet": { + "ID": "magnet", + "Type": "powerup", + "Filename": "", + "Scale": 1.0, + "ProcWidth": 50, + "ProcHeight": 50, + "DrawOffX": -25, + "DrawOffY": -50, + "Color": {"R": 255, "G": 80, "B": 220, "A": 255}, + "Hitbox": { + "OffsetX": 0, + "OffsetY": 0, + "W": 50, + "H": 50, + "Type": "" + } + }, "h-l": { "ID": "h-l", "Type": "obstacle", diff --git a/pkg/game/data.go b/pkg/game/data.go index 3e7ec03..4424a6c 100644 --- a/pkg/game/data.go +++ b/pkg/game/data.go @@ -91,21 +91,26 @@ type JoinRequest struct { } type PlayerState struct { - ID string `json:"id"` - Name string `json:"name"` - X float64 `json:"x"` - Y float64 `json:"y"` - VX float64 `json:"vx"` - VY float64 `json:"vy"` - State string `json:"state"` - OnGround bool `json:"on_ground"` - OnWall bool `json:"on_wall"` // Ist an einer Wand - LastInputSeq uint32 `json:"last_input_seq"` // Letzte verarbeitete Input-Sequenz - Score int `json:"score"` // Punkte des Spielers - IsAlive bool `json:"is_alive"` // Lebt der Spieler noch? - IsSpectator bool `json:"is_spectator"` // Ist im Zuschauer-Modus - HasDoubleJump bool `json:"has_double_jump"` // Hat Double Jump Powerup - HasGodMode bool `json:"has_godmode"` // Hat Godmode Powerup + ID string `json:"id"` + Name string `json:"name"` + X float64 `json:"x"` + Y float64 `json:"y"` + VX float64 `json:"vx"` + VY float64 `json:"vy"` + State string `json:"state"` + OnGround bool `json:"on_ground"` + OnWall bool `json:"on_wall"` // Ist an einer Wand + LastInputSeq uint32 `json:"last_input_seq"` // Letzte verarbeitete Input-Sequenz + Score int `json:"score"` // Punkte des Spielers + IsAlive bool `json:"is_alive"` // Lebt der Spieler noch? + IsSpectator bool `json:"is_spectator"` // Ist im Zuschauer-Modus + HasDoubleJump bool `json:"has_double_jump"` // Hat Double Jump Powerup + DoubleJumpUsed bool `json:"double_jump_used"` // Wurde zweiter Sprung schon benutzt? + DoubleJumpRemainingSeconds float64 `json:"double_jump_remaining_seconds"` // Verbleibende Double-Jump-Zeit in Sekunden + HasGodMode bool `json:"has_godmode"` // Hat Godmode Powerup + GodModeRemainingSeconds float64 `json:"godmode_remaining_seconds"` // Verbleibende Godmode-Zeit in Sekunden + HasMagnet bool `json:"has_magnet"` // Hat Magnet Powerup + MagnetRemainingSeconds float64 `json:"magnet_remaining_seconds"` // Verbleibende Magnet-Zeit in Sekunden } type GameState struct { diff --git a/pkg/server/room.go b/pkg/server/room.go index d65b719..1528a01 100644 --- a/pkg/server/room.go +++ b/pkg/server/room.go @@ -32,10 +32,13 @@ type ServerPlayer struct { IsSpectator bool // Powerups - HasDoubleJump bool // Doppelsprung aktiv? - DoubleJumpUsed bool // Wurde zweiter Sprung schon benutzt? - HasGodMode bool // Godmode aktiv? - GodModeEndTime time.Time // Wann endet Godmode? + HasDoubleJump bool // Doppelsprung aktiv? + DoubleJumpUsed bool // Wurde zweiter Sprung schon benutzt? + DoubleJumpEndTime time.Time // Wann endet Double Jump? + HasGodMode bool // Godmode aktiv? + GodModeEndTime time.Time // Wann endet Godmode? + HasMagnet bool // Magnet aktiv? + MagnetEndTime time.Time // Wann endet Magnet? } type MovingPlatform struct { @@ -525,11 +528,29 @@ func (r *Room) Update() { // Powerup Kollision prüfen r.CheckPowerupCollision(p) + // Double Jump Timeout prüfen + if p.HasDoubleJump && time.Now().After(p.DoubleJumpEndTime) { + p.HasDoubleJump = false + p.DoubleJumpUsed = false + log.Printf("⚡ Double Jump von %s ist abgelaufen", p.Name) + } + // Godmode Timeout prüfen if p.HasGodMode && time.Now().After(p.GodModeEndTime) { p.HasGodMode = false log.Printf("🛡️ Godmode von %s ist abgelaufen", p.Name) } + + // Magnet Timeout prüfen + if p.HasMagnet && time.Now().After(p.MagnetEndTime) { + p.HasMagnet = false + log.Printf("🧲 Magnet von %s ist abgelaufen", p.Name) + } + + // Magnet: Coins im Umkreis automatisch einsammeln + if p.HasMagnet { + r.ApplyMagnetEffect(p) + } } // 2b. Distanz-Score updaten @@ -838,22 +859,49 @@ func (r *Room) Broadcast() { DifficultyFactor: r.DifficultyFactor, } + now := time.Now() for id, p := range r.Players { + djRemaining := 0.0 + if p.HasDoubleJump { + djRemaining = p.DoubleJumpEndTime.Sub(now).Seconds() + if djRemaining < 0 { + djRemaining = 0 + } + } + godRemaining := 0.0 + if p.HasGodMode { + godRemaining = p.GodModeEndTime.Sub(now).Seconds() + if godRemaining < 0 { + godRemaining = 0 + } + } + magnetRemaining := 0.0 + if p.HasMagnet { + magnetRemaining = p.MagnetEndTime.Sub(now).Seconds() + if magnetRemaining < 0 { + magnetRemaining = 0 + } + } 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, + 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, + DoubleJumpUsed: p.DoubleJumpUsed, + DoubleJumpRemainingSeconds: djRemaining, + HasGodMode: p.HasGodMode, + GodModeRemainingSeconds: godRemaining, + HasMagnet: p.HasMagnet, + MagnetRemainingSeconds: magnetRemaining, } } diff --git a/pkg/server/scoring.go b/pkg/server/scoring.go index 5d87ab7..3a05e76 100644 --- a/pkg/server/scoring.go +++ b/pkg/server/scoring.go @@ -21,34 +21,23 @@ func (r *Room) CheckCoinCollision(p *ServerPlayer) { 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 { + if !ok || assetDef.Type != "coin" { 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 (muss DrawOffX/Y einbeziehen wie bei Obstacles!) coinHitbox := game.Rect{ OffsetX: activeChunk.X + obj.X + assetDef.DrawOffX + assetDef.Hitbox.OffsetX, OffsetY: obj.Y + assetDef.DrawOffY + assetDef.Hitbox.OffsetY, @@ -56,9 +45,7 @@ func (r *Room) CheckCoinCollision(p *ServerPlayer) { H: assetDef.Hitbox.H, } - // Kollision? if game.CheckRectCollision(playerHitbox, coinHitbox) { - // Coin einsammeln! r.CollectedCoins[coinKey] = true p.BonusScore += 200 p.Score = p.DistanceScore + p.BonusScore @@ -81,34 +68,23 @@ func (r *Room) CheckPowerupCollision(p *ServerPlayer) { 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 { + if !ok || assetDef.Type != "powerup" { 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, @@ -116,35 +92,77 @@ func (r *Room) CheckPowerupCollision(p *ServerPlayer) { 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) + p.DoubleJumpEndTime = time.Now().Add(15 * time.Second) + log.Printf("⚡ %s hat Double Jump erhalten! (15 Sekunden)", 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) + + case "magnet": + p.HasMagnet = true + p.MagnetEndTime = time.Now().Add(8 * time.Second) + log.Printf("🧲 %s hat Magnet erhalten! (8 Sekunden)", p.Name) } } } } } +// ApplyMagnetEffect sammelt alle Coins im Umkreis von ~300px automatisch ein +func (r *Room) ApplyMagnetEffect(p *ServerPlayer) { + const magnetRadius = 300.0 + + playerCenterX := p.X + r.pDrawOffX + r.pHitboxOffX + r.pW/2 + playerCenterY := p.Y + r.pDrawOffY + r.pHitboxOffY + r.pH/2 + + for _, activeChunk := range r.ActiveChunks { + chunkDef, exists := r.World.ChunkLibrary[activeChunk.ChunkID] + if !exists { + continue + } + + for objIdx, obj := range chunkDef.Objects { + assetDef, ok := r.World.Manifest.Assets[obj.AssetID] + if !ok || assetDef.Type != "coin" { + continue + } + + coinKey := fmt.Sprintf("%s_%d", activeChunk.ChunkID, objIdx) + if r.CollectedCoins[coinKey] { + continue + } + + coinCenterX := activeChunk.X + obj.X + assetDef.DrawOffX + assetDef.Hitbox.OffsetX + assetDef.Hitbox.W/2 + coinCenterY := obj.Y + assetDef.DrawOffY + assetDef.Hitbox.OffsetY + assetDef.Hitbox.H/2 + + dx := coinCenterX - playerCenterX + dy := coinCenterY - playerCenterY + + if dx*dx+dy*dy <= magnetRadius*magnetRadius { + r.CollectedCoins[coinKey] = true + p.BonusScore += 200 + p.Score = p.DistanceScore + p.BonusScore + log.Printf("🧲 %s hat Coin per Magnet eingesammelt! Score: %d", p.Name, p.Score) + } + } + } +} + // UpdateDistanceScore aktualisiert Distanz-basierte Punkte func (r *Room) UpdateDistanceScore() { if r.Status != "RUNNING" { return } - // Zähle lebende Spieler aliveCount := 0 for _, p := range r.Players { if p.IsAlive && !p.IsSpectator { @@ -156,15 +174,9 @@ func (r *Room) UpdateDistanceScore() { return } - // Pro lebendem Spieler werden Punkte hinzugefügt - // Dies akkumuliert die Punkte: mehr Spieler = schnellere Punktesammlung - // Jeder Tick (bei 60 FPS) fügt aliveCount Punkte hinzu - pointsToAdd := aliveCount - - // Jeder lebende Spieler bekommt die gleichen Punkte for _, p := range r.Players { if p.IsAlive && !p.IsSpectator { - p.DistanceScore += pointsToAdd + p.DistanceScore += aliveCount p.Score = p.DistanceScore + p.BonusScore } } @@ -180,7 +192,6 @@ func (r *Room) KillPlayer(p *ServerPlayer) { 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 {