Private
Public Access
1
0
Files
EscapeFromTeacher/cmd/client/particles.go
Sebastian Unterschütz 6d0d31824e
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 6m49s
fix game
2026-03-22 18:46:54 +01:00

380 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
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
}