diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index a0c7d03..3f560df 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -35,11 +35,13 @@ jobs: APP_URL="${{ env.BASE_DOMAIN }}" TARGET_NS="${REPO_LOWER}" BUILD_MODE="main" + CERT_ISSUER="letsencrypt-prod" echo "Mode: PRODUCTION (Root Domain)" else APP_URL="${REPO_LOWER}-${BRANCH_LOWER}.${{ env.BASE_DOMAIN }}" TARGET_NS="${REPO_LOWER}-${BRANCH_LOWER}" BUILD_MODE="dev" + CERT_ISSUER="letsencrypt-prod" echo "Mode: DEVELOPMENT (Subdomain)" fi @@ -53,6 +55,7 @@ jobs: echo "DEBUG: URL: $APP_URL" echo "DEBUG: Branch-Tag: $BRANCH_TAG" echo "DEBUG: Build-Mode: $BUILD_MODE" + echo "DEBUG: Cert-Issuer: $CERT_ISSUER" # In Gitea Actions Environment schreiben echo "FULL_IMAGE_PATH=$FULL_IMAGE_PATH" >> $GITHUB_ENV @@ -62,6 +65,7 @@ jobs: echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV echo "BRANCH_TAG=$BRANCH_TAG" >> $GITHUB_ENV echo "BUILD_MODE=$BUILD_MODE" >> $GITHUB_ENV + echo "CERT_ISSUER=$CERT_ISSUER" >> $GITHUB_ENV # 3. Prüfen ob ein Image-Rebuild nötig ist - name: Detect Source Changes @@ -170,6 +174,9 @@ jobs: # TARGET_NS überall ersetzen (z.B. für Middlewares oder explizite Namespaces) find k8s/ -name "*.yaml" -exec sed -i "s|\${TARGET_NS}|${{ env.TARGET_NS }}|g" {} + + + # CERT_ISSUER in allen K8s-Manifesten ersetzen + find k8s/ -name "*.yaml" -exec sed -i "s|\${CERT_ISSUER}|${{ env.CERT_ISSUER }}|g" {} + # Admin-Credentials Secret anlegen/aktualisieren (aus Gitea Secret) kubectl create secret generic admin-credentials \ diff --git a/cmd/client/audio.go b/cmd/client/audio.go index f493407..5bf77b7 100644 --- a/cmd/client/audio.go +++ b/cmd/client/audio.go @@ -51,8 +51,8 @@ func NewAudioSystem() *AudioSystem { as := &AudioSystem{ audioContext: ctx, - musicVolume: 0.3, // 30% Standard-Lautstärke - sfxVolume: 0.5, // 50% Standard-Lautstärke + musicVolume: 0.7, // 70% Standard-Lautstärke + sfxVolume: 0.3, // 30% Standard-Lautstärke muted: false, } diff --git a/cmd/client/offline_logic.go b/cmd/client/offline_logic.go index a67b3dc..0fdce8b 100644 --- a/cmd/client/offline_logic.go +++ b/cmd/client/offline_logic.go @@ -18,10 +18,11 @@ func (g *Game) startOfflineGame() { g.connected = false // Explizit offline g.appState = StateGame - // Initialen GameState lokal erstellen + // Initialen GameState lokal erstellen (mit Countdown) g.stateMutex.Lock() g.gameState = game.GameState{ - Status: "RUNNING", + Status: "COUNTDOWN", + TimeLeft: 3, RoomID: "offline_solo", Players: make(map[string]game.PlayerState), WorldChunks: []game.ActiveChunk{{ChunkID: "start", X: 0}}, @@ -30,7 +31,7 @@ func (g *Game) startOfflineGame() { CollectedCoins: make(map[string]bool), CollectedPowerups: make(map[string]bool), } - + // Lokalen Spieler hinzufügen g.gameState.Players[g.playerName] = game.PlayerState{ ID: g.playerName, @@ -48,28 +49,48 @@ func (g *Game) startOfflineGame() { log.Println("⚠️ Warnung: Keine Chunks in Library geladen!") } - g.roundStartTime = time.Now() + // Startzeit für Countdown + g.roundStartTime = time.Now().Add(3 * time.Second) g.predictedX = 100 g.predictedY = 200 - g.currentSpeed = config.RunSpeed - - g.audio.PlayMusic() + g.currentSpeed = 0 // Stillstand während Countdown + g.notifyGameStarted() - log.Println("🕹️ Offline-Modus gestartet") + log.Println("🕹️ Offline-Modus mit Countdown gestartet") } // updateOfflineLoop simuliert die Server-Logik lokal func (g *Game) updateOfflineLoop() { - if !g.isOffline || g.gameState.Status != "RUNNING" { + if !g.isOffline || g.gameState.Status == "GAMEOVER" { return } g.stateMutex.Lock() defer g.stateMutex.Unlock() + // 1. Status Logic (Countdown -> Running) + if g.gameState.Status == "COUNTDOWN" { + rem := time.Until(g.roundStartTime) + g.gameState.TimeLeft = int(rem.Seconds()) + 1 + + if rem <= 0 { + log.Println("🚀 Offline: GO!") + g.gameState.Status = "RUNNING" + g.gameState.TimeLeft = 0 + g.audio.PlayMusic() + // Reset roundStartTime auf den tatsächlichen Spielstart für Schwierigkeits-Skalierung + g.roundStartTime = time.Now() + } + return // Während Countdown keine weitere Logik (kein Scrolling, etc.) + } + + if g.gameState.Status != "RUNNING" { + return + } + elapsed := time.Since(g.roundStartTime).Seconds() - // 1. Schwierigkeit & Speed + // 2. Schwierigkeit & Speed g.gameState.DifficultyFactor = elapsed / config.MaxDifficultySeconds if g.gameState.DifficultyFactor > 1.0 { g.gameState.DifficultyFactor = 1.0 @@ -79,10 +100,10 @@ func (g *Game) updateOfflineLoop() { g.gameState.CurrentSpeed = config.RunSpeed + speedIncrease g.currentSpeed = g.gameState.CurrentSpeed - // 2. Scrolling + // 3. Scrolling g.gameState.ScrollX += g.currentSpeed - // 3. Chunks nachladen + // 4. Chunks nachladen mapEnd := 0.0 for _, c := range g.gameState.WorldChunks { chunkDef := g.world.ChunkLibrary[c.ChunkID] @@ -96,7 +117,7 @@ func (g *Game) updateOfflineLoop() { g.spawnOfflineChunk(mapEnd) } - // 4. Entferne alte Chunks + // 5. Entferne alte Chunks if len(g.gameState.WorldChunks) > 5 { if g.gameState.WorldChunks[0].X < g.gameState.ScrollX-2000 { // Bereinige auch Moving Platforms des alten Chunks @@ -112,15 +133,15 @@ func (g *Game) updateOfflineLoop() { } } - // 5. Update Moving Platforms + // 6. Update Moving Platforms g.updateOfflineMovingPlatforms() - // 6. Player State Update (Score, Powerups, Collisions) + // 7. Player State Update (Score, Powerups, Collisions) p, ok := g.gameState.Players[g.playerName] if ok && p.IsAlive { // 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 @@ -128,7 +149,7 @@ func (g *Game) updateOfflineLoop() { p.VY = g.predictedVY p.OnGround = g.predictedGround p.OnWall = g.predictedOnWall - + // Lokale Kollisionsprüfung für Coins/Powerups g.checkOfflineCollisions(&p) @@ -177,25 +198,25 @@ func (g *Game) spawnOfflineChunk(atX float64) { X: atX, }) - // Extrahiere Moving Platforms aus dem neuen Chunk + // Extrahiere Plattformen 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 + // In Solo gibt es keine MovingPlatformData, Plattformen sind statisch + if ok && asset.Type == "moving_platform" { 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, + StartX: atX + obj.X, + StartY: obj.Y, + EndX: atX + obj.X, + EndY: obj.Y, + Speed: 0, Direction: 1.0, - IsActive: true, - CurrentX: atX + mp.StartX, - CurrentY: mp.StartY, + IsActive: false, + CurrentX: atX + obj.X, + CurrentY: obj.Y, HitboxW: asset.Hitbox.W, HitboxH: asset.Hitbox.H, DrawOffX: asset.DrawOffX, @@ -256,7 +277,7 @@ func (g *Game) checkOfflineCollisions(p *game.PlayerState) { pDrawX = def.DrawOffX pDrawY = def.DrawOffY } - + pRect := game.Rect{ OffsetX: p.X + pDrawX + pOffX, OffsetY: p.Y + pDrawY + pOffY, @@ -268,25 +289,29 @@ func (g *Game) checkOfflineCollisions(p *game.PlayerState) { chunkDef := g.world.ChunkLibrary[ac.ChunkID] for i, obj := range chunkDef.Objects { asset, ok := g.world.Manifest.Assets[obj.AssetID] - if !ok { continue } + if !ok { + continue + } objID := fmt.Sprintf("%s_%d", ac.ChunkID, i) // 1. COINS if asset.Type == "coin" { - if g.gameState.CollectedCoins[objID] { continue } - + 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)) + + 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 @@ -306,8 +331,10 @@ func (g *Game) checkOfflineCollisions(p *game.PlayerState) { // 2. POWERUPS if asset.Type == "powerup" { - if g.gameState.CollectedPowerups[objID] { continue } - + 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, @@ -318,7 +345,7 @@ func (g *Game) checkOfflineCollisions(p *game.PlayerState) { if game.CheckRectCollision(pRect, puRect) { g.gameState.CollectedPowerups[objID] = true g.audio.PlayPowerUp() - + switch obj.AssetID { case "jumpboost": p.HasDoubleJump = true diff --git a/cmd/client/prediction.go b/cmd/client/prediction.go index 3e4dc4a..16ecaa0 100644 --- a/cmd/client/prediction.go +++ b/cmd/client/prediction.go @@ -12,6 +12,14 @@ import ( // ApplyInput wendet einen Input auf den vorhergesagten Zustand an // Nutzt die gemeinsame Physik-Engine aus pkg/physics func (g *Game) ApplyInput(input InputState) { + g.stateMutex.Lock() + status := g.gameState.Status + g.stateMutex.Unlock() + + if status == "COUNTDOWN" { + return + } + // Horizontale Bewegung mit analogem Joystick moveX := 0.0 if input.Left { @@ -203,40 +211,14 @@ func (g *Game) checkSoloRound() { return } - // 1. Lokale Todes-Erkennung (Obstacles & Grenzen) - // Wir nutzen die vorhergesagte Position - pConst := physics.DefaultPlayerConstants() - checkX := g.predictedX + pConst.DrawOffX + pConst.HitboxOffX - checkY := g.predictedY + pConst.DrawOffY + pConst.HitboxOffY - + // 1. Lokale Todes-Erkennung (Nur noch Grenzen im Solo-Modus) g.stateMutex.Lock() - collisionChecker := &physics.ClientCollisionChecker{ - World: g.world, - ActiveChunks: g.gameState.WorldChunks, - MovingPlatforms: g.gameState.MovingPlatforms, - } scrollX := g.gameState.ScrollX - - hasGodMode := false - for _, p := range g.gameState.Players { - if p.Name == g.playerName { - hasGodMode = p.HasGodMode - break - } - } g.stateMutex.Unlock() - // Kollision mit Hindernis? - hit, colType := collisionChecker.CheckCollision(checkX, checkY, pConst.Width, pConst.Height) - isDead := false deathReason := "" - if hit && colType == "obstacle" && !hasGodMode { - isDead = true - deathReason = "Hindernis berührt" - } - // Aus dem linken Bildschirmrand gefallen? if g.predictedX < scrollX-50 { isDead = true diff --git a/cmd/client/web/game.js b/cmd/client/web/game.js index 1e4b015..84326e4 100644 --- a/cmd/client/web/game.js +++ b/cmd/client/web/game.js @@ -630,8 +630,8 @@ function toggleAudio() { if (window.setSFXVolume) window.setSFXVolume(0); } else { btn.textContent = '🔊'; - const musicVol = parseInt(localStorage.getItem('escape_music_volume') || 70) / 100; - const sfxVol = parseInt(localStorage.getItem('escape_sfx_volume') || 70) / 100; + const musicVol = parseInt(localStorage.getItem('escape_music_volume') || 80) / 100; + const sfxVol = parseInt(localStorage.getItem('escape_sfx_volume') || 40) / 100; if (window.setMusicVolume) window.setMusicVolume(musicVol); if (window.setSFXVolume) window.setSFXVolume(sfxVol); } @@ -656,7 +656,7 @@ document.addEventListener('DOMContentLoaded', () => { }); // Load saved value - const savedMusic = localStorage.getItem('escape_music_volume') || 70; + const savedMusic = localStorage.getItem('escape_music_volume') || 80; musicSlider.value = savedMusic; musicValue.textContent = savedMusic + '%'; } @@ -673,7 +673,7 @@ document.addEventListener('DOMContentLoaded', () => { }); // Load saved value - const savedSFX = localStorage.getItem('escape_sfx_volume') || 70; + const savedSFX = localStorage.getItem('escape_sfx_volume') || 40; sfxSlider.value = savedSFX; sfxValue.textContent = savedSFX + '%'; } diff --git a/cmd/client/web/index.html b/cmd/client/web/index.html index 3beb599..9999281 100644 --- a/cmd/client/web/index.html +++ b/cmd/client/web/index.html @@ -95,14 +95,14 @@