enhance offline mode: add moving platforms, collision detection, and power-ups
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m58s
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m58s
This commit is contained in:
@@ -55,6 +55,27 @@ type InputState struct {
|
|||||||
JoyX float64 // Analoger Joystick-Wert (-1.0 bis 1.0)
|
JoyX float64 // Analoger Joystick-Wert (-1.0 bis 1.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MovingPlatform struct {
|
||||||
|
ChunkID string
|
||||||
|
ObjectIdx int
|
||||||
|
AssetID string
|
||||||
|
CurrentX float64
|
||||||
|
CurrentY float64
|
||||||
|
StartX float64
|
||||||
|
StartY float64
|
||||||
|
EndX float64
|
||||||
|
EndY float64
|
||||||
|
Speed float64
|
||||||
|
Direction float64
|
||||||
|
IsActive bool
|
||||||
|
HitboxW float64
|
||||||
|
HitboxH float64
|
||||||
|
DrawOffX float64
|
||||||
|
DrawOffY float64
|
||||||
|
HitboxOffX float64
|
||||||
|
HitboxOffY float64
|
||||||
|
}
|
||||||
|
|
||||||
// --- GAME STRUCT ---
|
// --- GAME STRUCT ---
|
||||||
type Game struct {
|
type Game struct {
|
||||||
appState int
|
appState int
|
||||||
@@ -74,6 +95,10 @@ type Game struct {
|
|||||||
activeField string // "name" oder "room" oder "teamname"
|
activeField string // "name" oder "room" oder "teamname"
|
||||||
gameMode string // "solo" oder "coop"
|
gameMode string // "solo" oder "coop"
|
||||||
isOffline bool // Läuft das Spiel lokal ohne Server?
|
isOffline bool // Läuft das Spiel lokal ohne Server?
|
||||||
|
offlineMovingPlatforms []*MovingPlatform // Lokale bewegende Plattformen für Offline-Modus
|
||||||
|
godModeEndTime time.Time
|
||||||
|
magnetEndTime time.Time
|
||||||
|
doubleJumpEndTime time.Time
|
||||||
isHost bool
|
isHost bool
|
||||||
teamName string // Team-Name für Coop beim Game Over
|
teamName string // Team-Name für Coop beim Game Over
|
||||||
|
|
||||||
@@ -876,6 +901,9 @@ func (g *Game) resetForNewGame() {
|
|||||||
|
|
||||||
// Spieler-State zurücksetzen
|
// Spieler-State zurücksetzen
|
||||||
g.isOffline = false
|
g.isOffline = false
|
||||||
|
g.godModeEndTime = time.Time{}
|
||||||
|
g.magnetEndTime = time.Time{}
|
||||||
|
g.doubleJumpEndTime = time.Time{}
|
||||||
g.scoreSubmitted = false
|
g.scoreSubmitted = false
|
||||||
g.lastStatus = ""
|
g.lastStatus = ""
|
||||||
g.correctionCount = 0
|
g.correctionCount = 0
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -25,6 +27,8 @@ func (g *Game) startOfflineGame() {
|
|||||||
WorldChunks: []game.ActiveChunk{{ChunkID: "start", X: 0}},
|
WorldChunks: []game.ActiveChunk{{ChunkID: "start", X: 0}},
|
||||||
CurrentSpeed: config.RunSpeed,
|
CurrentSpeed: config.RunSpeed,
|
||||||
DifficultyFactor: 0,
|
DifficultyFactor: 0,
|
||||||
|
CollectedCoins: make(map[string]bool),
|
||||||
|
CollectedPowerups: make(map[string]bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lokalen Spieler hinzufügen
|
// Lokalen Spieler hinzufügen
|
||||||
@@ -37,6 +41,8 @@ func (g *Game) startOfflineGame() {
|
|||||||
}
|
}
|
||||||
g.stateMutex.Unlock()
|
g.stateMutex.Unlock()
|
||||||
|
|
||||||
|
g.offlineMovingPlatforms = nil
|
||||||
|
|
||||||
// Initialer Chunk-Library Check
|
// Initialer Chunk-Library Check
|
||||||
if len(g.world.ChunkLibrary) == 0 {
|
if len(g.world.ChunkLibrary) == 0 {
|
||||||
log.Println("⚠️ Warnung: Keine Chunks in Library geladen!")
|
log.Println("⚠️ Warnung: Keine Chunks in Library geladen!")
|
||||||
@@ -63,7 +69,7 @@ func (g *Game) updateOfflineLoop() {
|
|||||||
|
|
||||||
elapsed := time.Since(g.roundStartTime).Seconds()
|
elapsed := time.Since(g.roundStartTime).Seconds()
|
||||||
|
|
||||||
// 1. Schwierigkeit & Speed (analog zu pkg/server/room.go)
|
// 1. Schwierigkeit & Speed
|
||||||
g.gameState.DifficultyFactor = elapsed / config.MaxDifficultySeconds
|
g.gameState.DifficultyFactor = elapsed / config.MaxDifficultySeconds
|
||||||
if g.gameState.DifficultyFactor > 1.0 {
|
if g.gameState.DifficultyFactor > 1.0 {
|
||||||
g.gameState.DifficultyFactor = 1.0
|
g.gameState.DifficultyFactor = 1.0
|
||||||
@@ -77,7 +83,6 @@ func (g *Game) updateOfflineLoop() {
|
|||||||
g.gameState.ScrollX += g.currentSpeed
|
g.gameState.ScrollX += g.currentSpeed
|
||||||
|
|
||||||
// 3. Chunks nachladen
|
// 3. Chunks nachladen
|
||||||
// Wenn das Ende der Map nah am rechten Rand ist, neuen Chunk spawnen
|
|
||||||
mapEnd := 0.0
|
mapEnd := 0.0
|
||||||
for _, c := range g.gameState.WorldChunks {
|
for _, c := range g.gameState.WorldChunks {
|
||||||
chunkDef := g.world.ChunkLibrary[c.ChunkID]
|
chunkDef := g.world.ChunkLibrary[c.ChunkID]
|
||||||
@@ -91,25 +96,73 @@ func (g *Game) updateOfflineLoop() {
|
|||||||
g.spawnOfflineChunk(mapEnd)
|
g.spawnOfflineChunk(mapEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Entferne alte Chunks (links aus dem Bild)
|
// 4. Entferne alte Chunks
|
||||||
if len(g.gameState.WorldChunks) > 5 {
|
if len(g.gameState.WorldChunks) > 5 {
|
||||||
// Behalte immer mindestens die letzten paar Chunks
|
|
||||||
if g.gameState.WorldChunks[0].X < g.gameState.ScrollX-2000 {
|
if g.gameState.WorldChunks[0].X < g.gameState.ScrollX-2000 {
|
||||||
|
// Bereinige auch Moving Platforms des alten Chunks
|
||||||
|
oldChunkID := g.gameState.WorldChunks[0].ChunkID
|
||||||
|
newPlats := g.offlineMovingPlatforms[:0]
|
||||||
|
for _, p := range g.offlineMovingPlatforms {
|
||||||
|
if p.ChunkID != oldChunkID {
|
||||||
|
newPlats = append(newPlats, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.offlineMovingPlatforms = newPlats
|
||||||
g.gameState.WorldChunks = g.gameState.WorldChunks[1:]
|
g.gameState.WorldChunks = g.gameState.WorldChunks[1:]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Score Update (Distanz)
|
// 5. Update Moving Platforms
|
||||||
|
g.updateOfflineMovingPlatforms()
|
||||||
|
|
||||||
|
// 6. Player State Update (Score, Powerups, Collisions)
|
||||||
p, ok := g.gameState.Players[g.playerName]
|
p, ok := g.gameState.Players[g.playerName]
|
||||||
if ok && p.IsAlive {
|
if ok && p.IsAlive {
|
||||||
// Grobe Score-Simulation
|
// Basis-Score aus Distanz
|
||||||
p.Score = int(g.gameState.ScrollX / 10)
|
p.Score = int(g.gameState.ScrollX / 10)
|
||||||
|
|
||||||
|
// Synchronisiere Prediction-State zurück in GameState (für Rendering)
|
||||||
|
p.X = g.predictedX
|
||||||
|
p.Y = g.predictedY
|
||||||
|
p.VX = g.predictedVX
|
||||||
|
p.VY = g.predictedVY
|
||||||
|
p.OnGround = g.predictedGround
|
||||||
|
p.OnWall = g.predictedOnWall
|
||||||
|
|
||||||
|
// Lokale Kollisionsprüfung für Coins/Powerups
|
||||||
|
g.checkOfflineCollisions(&p)
|
||||||
|
|
||||||
|
// Powerup-Timer herunterschalten
|
||||||
|
now := time.Now()
|
||||||
|
if p.HasGodMode && now.After(g.godModeEndTime) {
|
||||||
|
p.HasGodMode = false
|
||||||
|
}
|
||||||
|
if p.HasMagnet && now.After(g.magnetEndTime) {
|
||||||
|
p.HasMagnet = false
|
||||||
|
}
|
||||||
|
if g.predictedHasDoubleJump && now.After(g.doubleJumpEndTime) {
|
||||||
|
g.predictedHasDoubleJump = false
|
||||||
|
p.HasDoubleJump = false
|
||||||
|
}
|
||||||
|
|
||||||
g.gameState.Players[g.playerName] = p
|
g.gameState.Players[g.playerName] = p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Synchronisiere Plattform-Positionen für Renderer
|
||||||
|
syncPlats := make([]game.MovingPlatformSync, len(g.offlineMovingPlatforms))
|
||||||
|
for i, p := range g.offlineMovingPlatforms {
|
||||||
|
syncPlats[i] = game.MovingPlatformSync{
|
||||||
|
ChunkID: p.ChunkID,
|
||||||
|
ObjectIdx: p.ObjectIdx,
|
||||||
|
AssetID: p.AssetID,
|
||||||
|
X: p.CurrentX,
|
||||||
|
Y: p.CurrentY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
g.gameState.MovingPlatforms = syncPlats
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) spawnOfflineChunk(atX float64) {
|
func (g *Game) spawnOfflineChunk(atX float64) {
|
||||||
// Zufälligen Chunk wählen
|
|
||||||
var pool []string
|
var pool []string
|
||||||
for id := range g.world.ChunkLibrary {
|
for id := range g.world.ChunkLibrary {
|
||||||
if id != "start" {
|
if id != "start" {
|
||||||
@@ -123,5 +176,165 @@ func (g *Game) spawnOfflineChunk(atX float64) {
|
|||||||
ChunkID: randomID,
|
ChunkID: randomID,
|
||||||
X: atX,
|
X: atX,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Extrahiere Moving Platforms aus dem neuen Chunk
|
||||||
|
chunkDef := g.world.ChunkLibrary[randomID]
|
||||||
|
for i, obj := range chunkDef.Objects {
|
||||||
|
asset, ok := g.world.Manifest.Assets[obj.AssetID]
|
||||||
|
if ok && asset.Type == "moving_platform" && obj.MovingPlatform != nil {
|
||||||
|
mp := obj.MovingPlatform
|
||||||
|
p := &MovingPlatform{
|
||||||
|
ChunkID: randomID,
|
||||||
|
ObjectIdx: i,
|
||||||
|
AssetID: obj.AssetID,
|
||||||
|
StartX: atX + mp.StartX,
|
||||||
|
StartY: mp.StartY,
|
||||||
|
EndX: atX + mp.EndX,
|
||||||
|
EndY: mp.EndY,
|
||||||
|
Speed: mp.Speed,
|
||||||
|
Direction: 1.0,
|
||||||
|
IsActive: true,
|
||||||
|
CurrentX: atX + mp.StartX,
|
||||||
|
CurrentY: mp.StartY,
|
||||||
|
HitboxW: asset.Hitbox.W,
|
||||||
|
HitboxH: asset.Hitbox.H,
|
||||||
|
DrawOffX: asset.DrawOffX,
|
||||||
|
DrawOffY: asset.DrawOffY,
|
||||||
|
HitboxOffX: asset.Hitbox.OffsetX,
|
||||||
|
HitboxOffY: asset.Hitbox.OffsetY,
|
||||||
|
}
|
||||||
|
g.offlineMovingPlatforms = append(g.offlineMovingPlatforms, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) updateOfflineMovingPlatforms() {
|
||||||
|
for _, p := range g.offlineMovingPlatforms {
|
||||||
|
if !p.IsActive {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dx := p.EndX - p.StartX
|
||||||
|
dy := p.EndY - p.StartY
|
||||||
|
dist := math.Sqrt(dx*dx + dy*dy)
|
||||||
|
if dist < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
vx := (dx / dist) * (p.Speed / 20.0) * p.Direction
|
||||||
|
vy := (dy / dist) * (p.Speed / 20.0) * p.Direction
|
||||||
|
|
||||||
|
p.CurrentX += vx
|
||||||
|
p.CurrentY += vy
|
||||||
|
|
||||||
|
// Ziel erreicht? Umkehren.
|
||||||
|
if p.Direction > 0 {
|
||||||
|
dToEnd := math.Sqrt(math.Pow(p.CurrentX-p.EndX, 2) + math.Pow(p.CurrentY-p.EndY, 2))
|
||||||
|
if dToEnd < (p.Speed / 20.0) {
|
||||||
|
p.Direction = -1.0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dToStart := math.Sqrt(math.Pow(p.CurrentX-p.StartX, 2) + math.Pow(p.CurrentY-p.StartY, 2))
|
||||||
|
if dToStart < (p.Speed / 20.0) {
|
||||||
|
p.Direction = 1.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Game) checkOfflineCollisions(p *game.PlayerState) {
|
||||||
|
// Hitbox des Spielers (Welt-Koordinaten)
|
||||||
|
pW, pH := 40.0, 60.0 // Default
|
||||||
|
pOffX, pOffY := 0.0, 0.0
|
||||||
|
pDrawX, pDrawY := 0.0, 0.0
|
||||||
|
if def, ok := g.world.Manifest.Assets["player"]; ok {
|
||||||
|
pW = def.Hitbox.W
|
||||||
|
pH = def.Hitbox.H
|
||||||
|
pOffX = def.Hitbox.OffsetX
|
||||||
|
pOffY = def.Hitbox.OffsetY
|
||||||
|
pDrawX = def.DrawOffX
|
||||||
|
pDrawY = def.DrawOffY
|
||||||
|
}
|
||||||
|
|
||||||
|
pRect := game.Rect{
|
||||||
|
OffsetX: p.X + pDrawX + pOffX,
|
||||||
|
OffsetY: p.Y + pDrawY + pOffY,
|
||||||
|
W: pW,
|
||||||
|
H: pH,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ac := range g.gameState.WorldChunks {
|
||||||
|
chunkDef := g.world.ChunkLibrary[ac.ChunkID]
|
||||||
|
for i, obj := range chunkDef.Objects {
|
||||||
|
asset, ok := g.world.Manifest.Assets[obj.AssetID]
|
||||||
|
if !ok { continue }
|
||||||
|
|
||||||
|
objID := fmt.Sprintf("%s_%d", ac.ChunkID, i)
|
||||||
|
|
||||||
|
// 1. COINS
|
||||||
|
if asset.Type == "coin" {
|
||||||
|
if g.gameState.CollectedCoins[objID] { continue }
|
||||||
|
|
||||||
|
coinX := ac.X + obj.X + asset.DrawOffX + asset.Hitbox.OffsetX
|
||||||
|
coinY := obj.Y + asset.DrawOffY + asset.Hitbox.OffsetY
|
||||||
|
|
||||||
|
// Magnet-Effekt?
|
||||||
|
if p.HasMagnet {
|
||||||
|
playerCenterX := pRect.OffsetX + pRect.W/2
|
||||||
|
playerCenterY := pRect.OffsetY + pRect.H/2
|
||||||
|
coinCenterX := coinX + asset.Hitbox.W/2
|
||||||
|
coinCenterY := coinY + asset.Hitbox.H/2
|
||||||
|
|
||||||
|
dist := math.Sqrt(math.Pow(playerCenterX - coinCenterX, 2) + math.Pow(playerCenterY - coinCenterY, 2))
|
||||||
|
if dist < 300 {
|
||||||
|
// Münze wird eingesammelt wenn im Magnet-Radius
|
||||||
|
g.gameState.CollectedCoins[objID] = true
|
||||||
|
p.Score += 200 // Bonus direkt auf Score
|
||||||
|
g.audio.PlayCoin()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coinRect := game.Rect{OffsetX: coinX, OffsetY: coinY, W: asset.Hitbox.W, H: asset.Hitbox.H}
|
||||||
|
if game.CheckRectCollision(pRect, coinRect) {
|
||||||
|
g.gameState.CollectedCoins[objID] = true
|
||||||
|
p.Score += 200
|
||||||
|
g.audio.PlayCoin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. POWERUPS
|
||||||
|
if asset.Type == "powerup" {
|
||||||
|
if g.gameState.CollectedPowerups[objID] { continue }
|
||||||
|
|
||||||
|
puRect := game.Rect{
|
||||||
|
OffsetX: ac.X + obj.X + asset.DrawOffX + asset.Hitbox.OffsetX,
|
||||||
|
OffsetY: obj.Y + asset.DrawOffY + asset.Hitbox.OffsetY,
|
||||||
|
W: asset.Hitbox.W,
|
||||||
|
H: asset.Hitbox.H,
|
||||||
|
}
|
||||||
|
|
||||||
|
if game.CheckRectCollision(pRect, puRect) {
|
||||||
|
g.gameState.CollectedPowerups[objID] = true
|
||||||
|
g.audio.PlayPowerUp()
|
||||||
|
|
||||||
|
switch obj.AssetID {
|
||||||
|
case "jumpboost":
|
||||||
|
p.HasDoubleJump = true
|
||||||
|
p.DoubleJumpUsed = false
|
||||||
|
g.predictedHasDoubleJump = true
|
||||||
|
g.predictedDoubleJumpUsed = false
|
||||||
|
g.doubleJumpEndTime = time.Now().Add(15 * time.Second)
|
||||||
|
case "godmode":
|
||||||
|
p.HasGodMode = true
|
||||||
|
g.godModeEndTime = time.Now().Add(10 * time.Second)
|
||||||
|
case "magnet":
|
||||||
|
p.HasMagnet = true
|
||||||
|
g.magnetEndTime = time.Now().Add(8 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user