337 lines
8.8 KiB
Go
337 lines
8.8 KiB
Go
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
|
|
}
|