Private
Public Access
1
0

update workflows and game logic: add CERT_ISSUER support, enhance offline mode with countdown, and adjust default audio settings
All checks were successful
Dynamic Branch Deploy / build-and-deploy (push) Successful in 1m50s

This commit is contained in:
Sebastian Unterschütz
2026-04-22 19:27:21 +02:00
parent 9742ccb038
commit 8454557f16
7 changed files with 92 additions and 76 deletions

View File

@@ -35,11 +35,13 @@ jobs:
APP_URL="${{ env.BASE_DOMAIN }}" APP_URL="${{ env.BASE_DOMAIN }}"
TARGET_NS="${REPO_LOWER}" TARGET_NS="${REPO_LOWER}"
BUILD_MODE="main" BUILD_MODE="main"
CERT_ISSUER="letsencrypt-prod"
echo "Mode: PRODUCTION (Root Domain)" echo "Mode: PRODUCTION (Root Domain)"
else else
APP_URL="${REPO_LOWER}-${BRANCH_LOWER}.${{ env.BASE_DOMAIN }}" APP_URL="${REPO_LOWER}-${BRANCH_LOWER}.${{ env.BASE_DOMAIN }}"
TARGET_NS="${REPO_LOWER}-${BRANCH_LOWER}" TARGET_NS="${REPO_LOWER}-${BRANCH_LOWER}"
BUILD_MODE="dev" BUILD_MODE="dev"
CERT_ISSUER="letsencrypt-prod"
echo "Mode: DEVELOPMENT (Subdomain)" echo "Mode: DEVELOPMENT (Subdomain)"
fi fi
@@ -53,6 +55,7 @@ jobs:
echo "DEBUG: URL: $APP_URL" echo "DEBUG: URL: $APP_URL"
echo "DEBUG: Branch-Tag: $BRANCH_TAG" echo "DEBUG: Branch-Tag: $BRANCH_TAG"
echo "DEBUG: Build-Mode: $BUILD_MODE" echo "DEBUG: Build-Mode: $BUILD_MODE"
echo "DEBUG: Cert-Issuer: $CERT_ISSUER"
# In Gitea Actions Environment schreiben # In Gitea Actions Environment schreiben
echo "FULL_IMAGE_PATH=$FULL_IMAGE_PATH" >> $GITHUB_ENV echo "FULL_IMAGE_PATH=$FULL_IMAGE_PATH" >> $GITHUB_ENV
@@ -62,6 +65,7 @@ jobs:
echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
echo "BRANCH_TAG=$BRANCH_TAG" >> $GITHUB_ENV echo "BRANCH_TAG=$BRANCH_TAG" >> $GITHUB_ENV
echo "BUILD_MODE=$BUILD_MODE" >> $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 # 3. Prüfen ob ein Image-Rebuild nötig ist
- name: Detect Source Changes - name: Detect Source Changes
@@ -170,6 +174,9 @@ jobs:
# TARGET_NS überall ersetzen (z.B. für Middlewares oder explizite Namespaces) # 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" {} + 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) # Admin-Credentials Secret anlegen/aktualisieren (aus Gitea Secret)
kubectl create secret generic admin-credentials \ kubectl create secret generic admin-credentials \

View File

@@ -51,8 +51,8 @@ func NewAudioSystem() *AudioSystem {
as := &AudioSystem{ as := &AudioSystem{
audioContext: ctx, audioContext: ctx,
musicVolume: 0.3, // 30% Standard-Lautstärke musicVolume: 0.7, // 70% Standard-Lautstärke
sfxVolume: 0.5, // 50% Standard-Lautstärke sfxVolume: 0.3, // 30% Standard-Lautstärke
muted: false, muted: false,
} }

View File

@@ -18,10 +18,11 @@ func (g *Game) startOfflineGame() {
g.connected = false // Explizit offline g.connected = false // Explizit offline
g.appState = StateGame g.appState = StateGame
// Initialen GameState lokal erstellen // Initialen GameState lokal erstellen (mit Countdown)
g.stateMutex.Lock() g.stateMutex.Lock()
g.gameState = game.GameState{ g.gameState = game.GameState{
Status: "RUNNING", Status: "COUNTDOWN",
TimeLeft: 3,
RoomID: "offline_solo", RoomID: "offline_solo",
Players: make(map[string]game.PlayerState), Players: make(map[string]game.PlayerState),
WorldChunks: []game.ActiveChunk{{ChunkID: "start", X: 0}}, WorldChunks: []game.ActiveChunk{{ChunkID: "start", X: 0}},
@@ -30,7 +31,7 @@ func (g *Game) startOfflineGame() {
CollectedCoins: make(map[string]bool), CollectedCoins: make(map[string]bool),
CollectedPowerups: make(map[string]bool), CollectedPowerups: make(map[string]bool),
} }
// Lokalen Spieler hinzufügen // Lokalen Spieler hinzufügen
g.gameState.Players[g.playerName] = game.PlayerState{ g.gameState.Players[g.playerName] = game.PlayerState{
ID: g.playerName, ID: g.playerName,
@@ -48,28 +49,48 @@ func (g *Game) startOfflineGame() {
log.Println("⚠️ Warnung: Keine Chunks in Library geladen!") 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.predictedX = 100
g.predictedY = 200 g.predictedY = 200
g.currentSpeed = config.RunSpeed g.currentSpeed = 0 // Stillstand während Countdown
g.audio.PlayMusic()
g.notifyGameStarted() g.notifyGameStarted()
log.Println("🕹️ Offline-Modus gestartet") log.Println("🕹️ Offline-Modus mit Countdown gestartet")
} }
// updateOfflineLoop simuliert die Server-Logik lokal // updateOfflineLoop simuliert die Server-Logik lokal
func (g *Game) updateOfflineLoop() { func (g *Game) updateOfflineLoop() {
if !g.isOffline || g.gameState.Status != "RUNNING" { if !g.isOffline || g.gameState.Status == "GAMEOVER" {
return return
} }
g.stateMutex.Lock() g.stateMutex.Lock()
defer g.stateMutex.Unlock() 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() elapsed := time.Since(g.roundStartTime).Seconds()
// 1. Schwierigkeit & Speed // 2. 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
@@ -79,10 +100,10 @@ func (g *Game) updateOfflineLoop() {
g.gameState.CurrentSpeed = config.RunSpeed + speedIncrease g.gameState.CurrentSpeed = config.RunSpeed + speedIncrease
g.currentSpeed = g.gameState.CurrentSpeed g.currentSpeed = g.gameState.CurrentSpeed
// 2. Scrolling // 3. Scrolling
g.gameState.ScrollX += g.currentSpeed g.gameState.ScrollX += g.currentSpeed
// 3. Chunks nachladen // 4. Chunks nachladen
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]
@@ -96,7 +117,7 @@ func (g *Game) updateOfflineLoop() {
g.spawnOfflineChunk(mapEnd) g.spawnOfflineChunk(mapEnd)
} }
// 4. Entferne alte Chunks // 5. Entferne alte Chunks
if len(g.gameState.WorldChunks) > 5 { if len(g.gameState.WorldChunks) > 5 {
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 // 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() g.updateOfflineMovingPlatforms()
// 6. Player State Update (Score, Powerups, Collisions) // 7. 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 {
// Basis-Score aus Distanz // 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) // Synchronisiere Prediction-State zurück in GameState (für Rendering)
p.X = g.predictedX p.X = g.predictedX
p.Y = g.predictedY p.Y = g.predictedY
@@ -128,7 +149,7 @@ func (g *Game) updateOfflineLoop() {
p.VY = g.predictedVY p.VY = g.predictedVY
p.OnGround = g.predictedGround p.OnGround = g.predictedGround
p.OnWall = g.predictedOnWall p.OnWall = g.predictedOnWall
// Lokale Kollisionsprüfung für Coins/Powerups // Lokale Kollisionsprüfung für Coins/Powerups
g.checkOfflineCollisions(&p) g.checkOfflineCollisions(&p)
@@ -177,25 +198,25 @@ func (g *Game) spawnOfflineChunk(atX float64) {
X: atX, X: atX,
}) })
// Extrahiere Moving Platforms aus dem neuen Chunk // Extrahiere Plattformen aus dem neuen Chunk
chunkDef := g.world.ChunkLibrary[randomID] chunkDef := g.world.ChunkLibrary[randomID]
for i, obj := range chunkDef.Objects { for i, obj := range chunkDef.Objects {
asset, ok := g.world.Manifest.Assets[obj.AssetID] asset, ok := g.world.Manifest.Assets[obj.AssetID]
if ok && asset.Type == "moving_platform" && obj.MovingPlatform != nil { // In Solo gibt es keine MovingPlatformData, Plattformen sind statisch
mp := obj.MovingPlatform if ok && asset.Type == "moving_platform" {
p := &MovingPlatform{ p := &MovingPlatform{
ChunkID: randomID, ChunkID: randomID,
ObjectIdx: i, ObjectIdx: i,
AssetID: obj.AssetID, AssetID: obj.AssetID,
StartX: atX + mp.StartX, StartX: atX + obj.X,
StartY: mp.StartY, StartY: obj.Y,
EndX: atX + mp.EndX, EndX: atX + obj.X,
EndY: mp.EndY, EndY: obj.Y,
Speed: mp.Speed, Speed: 0,
Direction: 1.0, Direction: 1.0,
IsActive: true, IsActive: false,
CurrentX: atX + mp.StartX, CurrentX: atX + obj.X,
CurrentY: mp.StartY, CurrentY: obj.Y,
HitboxW: asset.Hitbox.W, HitboxW: asset.Hitbox.W,
HitboxH: asset.Hitbox.H, HitboxH: asset.Hitbox.H,
DrawOffX: asset.DrawOffX, DrawOffX: asset.DrawOffX,
@@ -256,7 +277,7 @@ func (g *Game) checkOfflineCollisions(p *game.PlayerState) {
pDrawX = def.DrawOffX pDrawX = def.DrawOffX
pDrawY = def.DrawOffY pDrawY = def.DrawOffY
} }
pRect := game.Rect{ pRect := game.Rect{
OffsetX: p.X + pDrawX + pOffX, OffsetX: p.X + pDrawX + pOffX,
OffsetY: p.Y + pDrawY + pOffY, OffsetY: p.Y + pDrawY + pOffY,
@@ -268,25 +289,29 @@ func (g *Game) checkOfflineCollisions(p *game.PlayerState) {
chunkDef := g.world.ChunkLibrary[ac.ChunkID] chunkDef := g.world.ChunkLibrary[ac.ChunkID]
for i, obj := range chunkDef.Objects { for i, obj := range chunkDef.Objects {
asset, ok := g.world.Manifest.Assets[obj.AssetID] asset, ok := g.world.Manifest.Assets[obj.AssetID]
if !ok { continue } if !ok {
continue
}
objID := fmt.Sprintf("%s_%d", ac.ChunkID, i) objID := fmt.Sprintf("%s_%d", ac.ChunkID, i)
// 1. COINS // 1. COINS
if asset.Type == "coin" { 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 coinX := ac.X + obj.X + asset.DrawOffX + asset.Hitbox.OffsetX
coinY := obj.Y + asset.DrawOffY + asset.Hitbox.OffsetY coinY := obj.Y + asset.DrawOffY + asset.Hitbox.OffsetY
// Magnet-Effekt? // Magnet-Effekt?
if p.HasMagnet { if p.HasMagnet {
playerCenterX := pRect.OffsetX + pRect.W/2 playerCenterX := pRect.OffsetX + pRect.W/2
playerCenterY := pRect.OffsetY + pRect.H/2 playerCenterY := pRect.OffsetY + pRect.H/2
coinCenterX := coinX + asset.Hitbox.W/2 coinCenterX := coinX + asset.Hitbox.W/2
coinCenterY := coinY + asset.Hitbox.H/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 { if dist < 300 {
// Münze wird eingesammelt wenn im Magnet-Radius // Münze wird eingesammelt wenn im Magnet-Radius
g.gameState.CollectedCoins[objID] = true g.gameState.CollectedCoins[objID] = true
@@ -306,8 +331,10 @@ func (g *Game) checkOfflineCollisions(p *game.PlayerState) {
// 2. POWERUPS // 2. POWERUPS
if asset.Type == "powerup" { if asset.Type == "powerup" {
if g.gameState.CollectedPowerups[objID] { continue } if g.gameState.CollectedPowerups[objID] {
continue
}
puRect := game.Rect{ puRect := game.Rect{
OffsetX: ac.X + obj.X + asset.DrawOffX + asset.Hitbox.OffsetX, OffsetX: ac.X + obj.X + asset.DrawOffX + asset.Hitbox.OffsetX,
OffsetY: obj.Y + asset.DrawOffY + asset.Hitbox.OffsetY, OffsetY: obj.Y + asset.DrawOffY + asset.Hitbox.OffsetY,
@@ -318,7 +345,7 @@ func (g *Game) checkOfflineCollisions(p *game.PlayerState) {
if game.CheckRectCollision(pRect, puRect) { if game.CheckRectCollision(pRect, puRect) {
g.gameState.CollectedPowerups[objID] = true g.gameState.CollectedPowerups[objID] = true
g.audio.PlayPowerUp() g.audio.PlayPowerUp()
switch obj.AssetID { switch obj.AssetID {
case "jumpboost": case "jumpboost":
p.HasDoubleJump = true p.HasDoubleJump = true

View File

@@ -12,6 +12,14 @@ import (
// ApplyInput wendet einen Input auf den vorhergesagten Zustand an // ApplyInput wendet einen Input auf den vorhergesagten Zustand an
// Nutzt die gemeinsame Physik-Engine aus pkg/physics // Nutzt die gemeinsame Physik-Engine aus pkg/physics
func (g *Game) ApplyInput(input InputState) { 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 // Horizontale Bewegung mit analogem Joystick
moveX := 0.0 moveX := 0.0
if input.Left { if input.Left {
@@ -203,40 +211,14 @@ func (g *Game) checkSoloRound() {
return return
} }
// 1. Lokale Todes-Erkennung (Obstacles & Grenzen) // 1. Lokale Todes-Erkennung (Nur noch Grenzen im Solo-Modus)
// Wir nutzen die vorhergesagte Position
pConst := physics.DefaultPlayerConstants()
checkX := g.predictedX + pConst.DrawOffX + pConst.HitboxOffX
checkY := g.predictedY + pConst.DrawOffY + pConst.HitboxOffY
g.stateMutex.Lock() g.stateMutex.Lock()
collisionChecker := &physics.ClientCollisionChecker{
World: g.world,
ActiveChunks: g.gameState.WorldChunks,
MovingPlatforms: g.gameState.MovingPlatforms,
}
scrollX := g.gameState.ScrollX scrollX := g.gameState.ScrollX
hasGodMode := false
for _, p := range g.gameState.Players {
if p.Name == g.playerName {
hasGodMode = p.HasGodMode
break
}
}
g.stateMutex.Unlock() g.stateMutex.Unlock()
// Kollision mit Hindernis?
hit, colType := collisionChecker.CheckCollision(checkX, checkY, pConst.Width, pConst.Height)
isDead := false isDead := false
deathReason := "" deathReason := ""
if hit && colType == "obstacle" && !hasGodMode {
isDead = true
deathReason = "Hindernis berührt"
}
// Aus dem linken Bildschirmrand gefallen? // Aus dem linken Bildschirmrand gefallen?
if g.predictedX < scrollX-50 { if g.predictedX < scrollX-50 {
isDead = true isDead = true

View File

@@ -630,8 +630,8 @@ function toggleAudio() {
if (window.setSFXVolume) window.setSFXVolume(0); if (window.setSFXVolume) window.setSFXVolume(0);
} else { } else {
btn.textContent = '🔊'; btn.textContent = '🔊';
const musicVol = parseInt(localStorage.getItem('escape_music_volume') || 70) / 100; const musicVol = parseInt(localStorage.getItem('escape_music_volume') || 80) / 100;
const sfxVol = parseInt(localStorage.getItem('escape_sfx_volume') || 70) / 100; const sfxVol = parseInt(localStorage.getItem('escape_sfx_volume') || 40) / 100;
if (window.setMusicVolume) window.setMusicVolume(musicVol); if (window.setMusicVolume) window.setMusicVolume(musicVol);
if (window.setSFXVolume) window.setSFXVolume(sfxVol); if (window.setSFXVolume) window.setSFXVolume(sfxVol);
} }
@@ -656,7 +656,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
// Load saved value // Load saved value
const savedMusic = localStorage.getItem('escape_music_volume') || 70; const savedMusic = localStorage.getItem('escape_music_volume') || 80;
musicSlider.value = savedMusic; musicSlider.value = savedMusic;
musicValue.textContent = savedMusic + '%'; musicValue.textContent = savedMusic + '%';
} }
@@ -673,7 +673,7 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
// Load saved value // Load saved value
const savedSFX = localStorage.getItem('escape_sfx_volume') || 70; const savedSFX = localStorage.getItem('escape_sfx_volume') || 40;
sfxSlider.value = savedSFX; sfxSlider.value = savedSFX;
sfxValue.textContent = savedSFX + '%'; sfxValue.textContent = savedSFX + '%';
} }

View File

@@ -95,14 +95,14 @@
<div class="settings-group"> <div class="settings-group">
<div class="setting-item"> <div class="setting-item">
<label>MUSIK LAUTSTÄRKE:</label> <label>MUSIK LAUTSTÄRKE:</label>
<input type="range" id="musicVolume" min="0" max="100" value="70"> <input type="range" id="musicVolume" min="0" max="100" value="80">
<span id="musicValue">70%</span> <span id="musicValue">80%</span>
</div> </div>
<div class="setting-item"> <div class="setting-item">
<label>SFX LAUTSTÄRKE:</label> <label>SFX LAUTSTÄRKE:</label>
<input type="range" id="sfxVolume" min="0" max="100" value="70"> <input type="range" id="sfxVolume" min="0" max="100" value="40">
<span id="sfxValue">70%</span> <span id="sfxValue">40%</span>
</div> </div>
</div> </div>

View File

@@ -3,7 +3,7 @@ kind: Ingress
metadata: metadata:
name: game-ingress name: game-ingress
annotations: annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod cert-manager.io/cluster-issuer: ${CERT_ISSUER}
traefik.ingress.kubernetes.io/router.entrypoints: web, websecure traefik.ingress.kubernetes.io/router.entrypoints: web, websecure
traefik.ingress.kubernetes.io/router.middlewares: gitea-redirect-https@kubernetescrd,${TARGET_NS}-compress@kubernetescrd traefik.ingress.kubernetes.io/router.middlewares: gitea-redirect-https@kubernetescrd,${TARGET_NS}-compress@kubernetescrd
spec: spec: