All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 7m13s
380 lines
10 KiB
Go
380 lines
10 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.
|
||
// 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()
|
||
|
||
// 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
|
||
case "magnet":
|
||
particleColor = color.RGBA{255, 80, 220, 200} // Pink/Magenta
|
||
default:
|
||
particleColor = color.RGBA{200, 100, 255, 200} // Lila
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
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,
|
||
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) * intensity,
|
||
Size: size,
|
||
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,
|
||
})
|
||
}
|
||
g.TriggerShake(12, 7.0)
|
||
}
|
||
|
||
// 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()
|
||
|
||
// Canvas-Höhe für Y-Transformation
|
||
_, canvasH := screen.Size()
|
||
|
||
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 und mit Y-Transformation
|
||
screenX := float32((p.X - g.camX) * GetScaleFromHeight(canvasH))
|
||
screenY := float32(WorldToScreenYWithHeight(p.Y, canvasH))
|
||
|
||
// 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 {
|
||
// 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 {
|
||
// 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)
|
||
}
|
||
}
|
||
|
||
// 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
|
||
}
|