package main import ( "fmt" "image/color" "math" "math/rand" "git.zb-server.de/ZB-Server/EscapeFromTeacher/pkg/game" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) // Particle definiert ein einzelnes Partikel type Particle struct { X, Y float64 VX, VY float64 Life float64 // 0.0 bis 1.0 (1.0 = neu, 0.0 = tot) MaxLife float64 // Ursprüngliche Lebensdauer in Sekunden Size float64 Color color.RGBA Type string // "coin", "powerup", "landing", "death" Gravity bool // Soll Gravitation angewendet werden? FadeOut bool // Soll ausblenden? } // SpawnCoinParticles erstellt Partikel für Coin-Einsammlung func (g *Game) SpawnCoinParticles(x, y float64) { g.particlesMutex.Lock() defer g.particlesMutex.Unlock() // 10-15 goldene Partikel - spawnen in alle Richtungen count := 10 + rand.Intn(6) for i := 0; i < count; i++ { angle := rand.Float64() * 2 * math.Pi speed := 2.0 + rand.Float64()*3.0 g.particles = append(g.particles, Particle{ X: x, Y: y, VX: math.Cos(angle) * speed, VY: math.Sin(angle) * speed, Life: 1.0, MaxLife: 0.5 + rand.Float64()*0.3, Size: 3.0 + rand.Float64()*2.0, Color: color.RGBA{255, 215, 0, 255}, // Gold Type: "coin", Gravity: true, FadeOut: true, }) } } // SpawnPowerupAura erstellt Partikel-Aura um Spieler mit aktivem Powerup func (g *Game) SpawnPowerupAura(x, y float64, powerupType string) { g.particlesMutex.Lock() defer g.particlesMutex.Unlock() // Farbe je nach Powerup-Typ var particleColor color.RGBA switch powerupType { case "doublejump": particleColor = color.RGBA{100, 200, 255, 200} // Hellblau case "godmode": particleColor = color.RGBA{255, 215, 0, 200} // Gold 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 { return } // 1 Partikel für sanfte Aura - spawnen in alle Richtungen angle := rand.Float64() * 2 * math.Pi distance := 25.0 + rand.Float64()*20.0 g.particles = append(g.particles, Particle{ X: x + math.Cos(angle)*distance, Y: y + math.Sin(angle)*distance, 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, Color: particleColor, Type: "powerup", Gravity: false, FadeOut: true, }) } // SpawnLandingParticles erstellt Staub-Partikel beim Landen func (g *Game) SpawnLandingParticles(x, y float64) { g.particlesMutex.Lock() defer g.particlesMutex.Unlock() // 5-8 Staub-Partikel count := 5 + rand.Intn(4) for i := 0; i < count; i++ { angle := math.Pi + (rand.Float64()-0.5)*0.8 // Nach unten/seitlich speed := 1.0 + rand.Float64()*2.0 g.particles = append(g.particles, Particle{ X: x + (rand.Float64()-0.5)*30.0, Y: y + 10, VX: math.Cos(angle) * speed, VY: math.Sin(angle) * speed, Life: 1.0, MaxLife: 0.3 + rand.Float64()*0.2, Size: 2.0 + rand.Float64()*2.0, Color: color.RGBA{150, 150, 150, 180}, // Grau Type: "landing", Gravity: false, FadeOut: true, }) } } // SpawnDeathParticles erstellt Explosions-Partikel beim Tod func (g *Game) SpawnDeathParticles(x, y float64) { g.particlesMutex.Lock() defer g.particlesMutex.Unlock() // 20-30 rote Partikel count := 20 + rand.Intn(11) for i := 0; i < count; i++ { angle := rand.Float64() * 2 * math.Pi speed := 3.0 + rand.Float64()*5.0 g.particles = append(g.particles, Particle{ X: x, Y: y + 10, VX: math.Cos(angle) * speed, VY: math.Sin(angle) * speed, Life: 1.0, MaxLife: 0.8 + rand.Float64()*0.4, Size: 3.0 + rand.Float64()*3.0, Color: color.RGBA{255, 50, 50, 255}, // Rot Type: "death", Gravity: true, FadeOut: true, }) } } // UpdateParticles aktualisiert alle Partikel func (g *Game) UpdateParticles(dt float64) { g.particlesMutex.Lock() defer g.particlesMutex.Unlock() // Filtern: Nur lebende Partikel behalten alive := make([]Particle, 0, len(g.particles)) for i := range g.particles { p := &g.particles[i] // Position updaten p.X += p.VX p.Y += p.VY // Gravitation if p.Gravity { p.VY += 0.3 // Gravitation } // Friction p.VX *= 0.98 p.VY *= 0.98 // Leben verringern p.Life -= dt / p.MaxLife // Nur behalten wenn noch am Leben if p.Life > 0 { alive = append(alive, *p) } } g.particles = alive } // RenderParticles zeichnet alle Partikel func (g *Game) RenderParticles(screen *ebiten.Image) { g.particlesMutex.Lock() defer g.particlesMutex.Unlock() for i := range g.particles { p := &g.particles[i] // Alpha-Wert basierend auf Leben alpha := uint8(255) if p.FadeOut { alpha = uint8(float64(p.Color.A) * p.Life) } col := color.RGBA{p.Color.R, p.Color.G, p.Color.B, alpha} // Position relativ zur Kamera screenX := float32(p.X - g.camX) screenY := float32(p.Y) // Partikel als Kreis zeichnen vector.DrawFilledCircle(screen, screenX, screenY, float32(p.Size), col, false) } } // DetectAndSpawnParticles prüft Game-State-Änderungen und spawnt Partikel func (g *Game) DetectAndSpawnParticles() { // Kopiere relevante Daten unter kurzen Lock g.stateMutex.Lock() // Kopiere Coins currentCoins := make(map[string]bool, len(g.gameState.CollectedCoins)) for k, v := range g.gameState.CollectedCoins { currentCoins[k] = v } // Kopiere Powerups currentPowerups := make(map[string]bool, len(g.gameState.CollectedPowerups)) for k, v := range g.gameState.CollectedPowerups { currentPowerups[k] = v } // Kopiere Spieler currentPlayers := make(map[string]game.PlayerState, len(g.gameState.Players)) for k, v := range g.gameState.Players { currentPlayers[k] = v } // Kopiere WorldChunks für Position-Lookup worldChunks := make([]game.ActiveChunk, len(g.gameState.WorldChunks)) copy(worldChunks, g.gameState.WorldChunks) g.stateMutex.Unlock() // Ab hier ohne Lock arbeiten // Alte Coins entfernen, die nicht mehr in currentCoins sind (Chunk wurde entfernt) for key := range g.lastCollectedCoins { if !currentCoins[key] { delete(g.lastCollectedCoins, key) } } // 1. Prüfe neue gesammelte Coins for coinKey := range currentCoins { if !g.lastCollectedCoins[coinKey] { // Neuer Coin gesammelt! if pos := g.findObjectPosition(coinKey, worldChunks, "coin"); pos != nil { g.SpawnCoinParticles(pos.X, pos.Y) g.audio.PlayCoin() // Coin Sound abspielen } g.lastCollectedCoins[coinKey] = true } } // Alte Powerups entfernen for key := range g.lastCollectedPowerups { if !currentPowerups[key] { delete(g.lastCollectedPowerups, key) } } // 2. Prüfe neue gesammelte Powerups for powerupKey := range currentPowerups { if !g.lastCollectedPowerups[powerupKey] { // Neues Powerup gesammelt! if pos := g.findObjectPosition(powerupKey, worldChunks, "powerup"); pos != nil { g.SpawnCoinParticles(pos.X, pos.Y) g.audio.PlayPowerUp() // PowerUp Sound abspielen } g.lastCollectedPowerups[powerupKey] = true } } // 3. Prüfe Spieler-Status und spawn Aura/Death Partikel for playerID, player := range currentPlayers { lastState, existed := g.lastPlayerStates[playerID] // Death Partikel if existed && lastState.IsAlive && !player.IsAlive { // Berechne Spieler-Mitte centerX := player.X - 56 + 68 + 73/2 centerY := player.Y - 231 + 42 + 184/2 g.SpawnDeathParticles(centerX, centerY) } // Powerup Aura (kontinuierlich) if player.IsAlive && !player.IsSpectator { // Berechne Spieler-Mitte mit Draw-Offsets // DrawOffX: -56, DrawOffY: -231 // Hitbox: OffsetX: 68, OffsetY: 42, W: 73, H: 184 centerX := player.X - 56 + 68 + 73/2 centerY := player.Y - 231 + 42 + 184/2 if player.HasDoubleJump { g.SpawnPowerupAura(centerX, centerY, "doublejump") } if player.HasGodMode { g.SpawnPowerupAura(centerX, centerY, "godmode") } } // State aktualisieren g.lastPlayerStates[playerID] = player } } // findObjectPosition finiert die Welt-Position eines Objects (Coin/Powerup) basierend auf Key func (g *Game) findObjectPosition(objectKey string, worldChunks []game.ActiveChunk, objectType string) *struct{ X, Y float64 } { for _, activeChunk := range worldChunks { chunkDef, exists := g.world.ChunkLibrary[activeChunk.ChunkID] if !exists { continue } for objIdx, obj := range chunkDef.Objects { key := fmt.Sprintf("%s_%d", activeChunk.ChunkID, objIdx) if key == objectKey { assetDef, ok := g.world.Manifest.Assets[obj.AssetID] if ok && assetDef.Type == objectType { // Berechne die Mitte der Hitbox centerX := activeChunk.X + obj.X + assetDef.DrawOffX + assetDef.Hitbox.OffsetX + assetDef.Hitbox.W/2 centerY := obj.Y + assetDef.DrawOffY + assetDef.Hitbox.OffsetY + assetDef.Hitbox.H/2 return &struct{ X, Y float64 }{ X: centerX, Y: centerY, } } } } } return nil }