Add platform-specific implementations for assets, audio, WebSocket, and rendering on Desktop and WebAssembly platforms. Introduce embedded assets for WebAssembly and native file handling for Desktop. Add platform-specific chunk loading and game state synchronization.
This commit is contained in:
336
cmd/client/particles.go
Normal file
336
cmd/client/particles.go
Normal file
@@ -0,0 +1,336 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user