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:
@@ -1,7 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
@@ -19,12 +21,14 @@ func (g *Game) startOfflineGame() {
|
||||
// Initialen GameState lokal erstellen
|
||||
g.stateMutex.Lock()
|
||||
g.gameState = game.GameState{
|
||||
Status: "RUNNING",
|
||||
RoomID: "offline_solo",
|
||||
Players: make(map[string]game.PlayerState),
|
||||
WorldChunks: []game.ActiveChunk{{ChunkID: "start", X: 0}},
|
||||
CurrentSpeed: config.RunSpeed,
|
||||
DifficultyFactor: 0,
|
||||
Status: "RUNNING",
|
||||
RoomID: "offline_solo",
|
||||
Players: make(map[string]game.PlayerState),
|
||||
WorldChunks: []game.ActiveChunk{{ChunkID: "start", X: 0}},
|
||||
CurrentSpeed: config.RunSpeed,
|
||||
DifficultyFactor: 0,
|
||||
CollectedCoins: make(map[string]bool),
|
||||
CollectedPowerups: make(map[string]bool),
|
||||
}
|
||||
|
||||
// Lokalen Spieler hinzufügen
|
||||
@@ -37,6 +41,8 @@ func (g *Game) startOfflineGame() {
|
||||
}
|
||||
g.stateMutex.Unlock()
|
||||
|
||||
g.offlineMovingPlatforms = nil
|
||||
|
||||
// Initialer Chunk-Library Check
|
||||
if len(g.world.ChunkLibrary) == 0 {
|
||||
log.Println("⚠️ Warnung: Keine Chunks in Library geladen!")
|
||||
@@ -63,7 +69,7 @@ func (g *Game) updateOfflineLoop() {
|
||||
|
||||
elapsed := time.Since(g.roundStartTime).Seconds()
|
||||
|
||||
// 1. Schwierigkeit & Speed (analog zu pkg/server/room.go)
|
||||
// 1. Schwierigkeit & Speed
|
||||
g.gameState.DifficultyFactor = elapsed / config.MaxDifficultySeconds
|
||||
if g.gameState.DifficultyFactor > 1.0 {
|
||||
g.gameState.DifficultyFactor = 1.0
|
||||
@@ -77,7 +83,6 @@ func (g *Game) updateOfflineLoop() {
|
||||
g.gameState.ScrollX += g.currentSpeed
|
||||
|
||||
// 3. Chunks nachladen
|
||||
// Wenn das Ende der Map nah am rechten Rand ist, neuen Chunk spawnen
|
||||
mapEnd := 0.0
|
||||
for _, c := range g.gameState.WorldChunks {
|
||||
chunkDef := g.world.ChunkLibrary[c.ChunkID]
|
||||
@@ -91,25 +96,73 @@ func (g *Game) updateOfflineLoop() {
|
||||
g.spawnOfflineChunk(mapEnd)
|
||||
}
|
||||
|
||||
// 4. Entferne alte Chunks (links aus dem Bild)
|
||||
// 4. Entferne alte Chunks
|
||||
if len(g.gameState.WorldChunks) > 5 {
|
||||
// Behalte immer mindestens die letzten paar Chunks
|
||||
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:]
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Score Update (Distanz)
|
||||
// 5. Update Moving Platforms
|
||||
g.updateOfflineMovingPlatforms()
|
||||
|
||||
// 6. Player State Update (Score, Powerups, Collisions)
|
||||
p, ok := g.gameState.Players[g.playerName]
|
||||
if ok && p.IsAlive {
|
||||
// Grobe Score-Simulation
|
||||
// Basis-Score aus Distanz
|
||||
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
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Zufälligen Chunk wählen
|
||||
var pool []string
|
||||
for id := range g.world.ChunkLibrary {
|
||||
if id != "start" {
|
||||
@@ -123,5 +176,165 @@ func (g *Game) spawnOfflineChunk(atX float64) {
|
||||
ChunkID: randomID,
|
||||
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