Private
Public Access
1
0

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

This commit is contained in:
Sebastian Unterschütz
2026-04-22 18:35:58 +02:00
parent fcf44ba513
commit 9742ccb038
2 changed files with 254 additions and 13 deletions

View File

@@ -55,6 +55,27 @@ type InputState struct {
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 ---
type Game struct {
appState int
@@ -74,6 +95,10 @@ type Game struct {
activeField string // "name" oder "room" oder "teamname"
gameMode string // "solo" oder "coop"
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
teamName string // Team-Name für Coop beim Game Over
@@ -876,6 +901,9 @@ func (g *Game) resetForNewGame() {
// Spieler-State zurücksetzen
g.isOffline = false
g.godModeEndTime = time.Time{}
g.magnetEndTime = time.Time{}
g.doubleJumpEndTime = time.Time{}
g.scoreSubmitted = false
g.lastStatus = ""
g.correctionCount = 0

View File

@@ -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)
}
}
}
}
}
}