Private
Public Access
1
0

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:
Sebastian Unterschütz
2026-01-04 01:25:04 +01:00
parent 85d697df19
commit 3232ee7c2f
86 changed files with 4931 additions and 486 deletions

336
cmd/client/particles.go Normal file
View 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
}