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 }}"
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
@@ -171,6 +175,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 \
--from-literal=username="${{ secrets.ADMIN_USER }}" \

View File

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

View File

@@ -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}},
@@ -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.currentSpeed = 0 // Stillstand während Countdown
g.audio.PlayMusic()
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,10 +133,10 @@ 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
@@ -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,
@@ -268,13 +289,17 @@ 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
@@ -306,7 +331,9 @@ 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,

View File

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

View File

@@ -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 + '%';
}

View File

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

View File

@@ -3,7 +3,7 @@ kind: Ingress
metadata:
name: game-ingress
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.middlewares: gitea-redirect-https@kubernetescrd,${TARGET_NS}-compress@kubernetescrd
spec: