fix: Restore all files lost during destructive rebase

A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,348 @@
// ==============================================
// AchievementManager.cs - Achievement System
// ==============================================
// Handles achievement display and notifications.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace BreakpilotDrive
{
[Serializable]
public class Achievement
{
public string id;
public string name;
public string description;
public string icon;
public string category;
public int threshold;
public int progress;
public bool unlocked;
}
[Serializable]
public class AchievementResponse
{
public string user_id;
public int total;
public int unlocked_count;
public Achievement[] achievements;
}
[Serializable]
private class AchievementArrayWrapper
{
public Achievement[] achievements;
}
public class AchievementManager : MonoBehaviour
{
public static AchievementManager Instance { get; private set; }
[Header("UI References")]
[SerializeField] private GameObject achievementPopupPrefab;
[SerializeField] private Transform popupContainer;
[SerializeField] private float popupDuration = 3f;
[Header("Audio")]
[SerializeField] private AudioClip unlockSound;
// Events
public UnityEvent<Achievement> OnAchievementUnlocked;
// Cached data
private List<Achievement> achievements = new List<Achievement>();
private HashSet<string> previouslyUnlocked = new HashSet<string>();
private Queue<Achievement> popupQueue = new Queue<Achievement>();
private bool isShowingPopup = false;
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
// ==============================================
// Load Achievements from API
// ==============================================
public IEnumerator LoadAchievements(string userId, Action<AchievementResponse> onComplete = null)
{
string url = $"{GetBaseUrl()}/achievements/{userId}";
using (var request = UnityEngine.Networking.UnityWebRequest.Get(url))
{
// Add auth header if available
string authHeader = AuthManager.Instance?.GetAuthHeader();
if (!string.IsNullOrEmpty(authHeader))
{
request.SetRequestHeader("Authorization", authHeader);
}
yield return request.SendWebRequest();
if (request.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
{
var response = JsonUtility.FromJson<AchievementResponse>(request.downloadHandler.text);
// Store previously unlocked for comparison
previouslyUnlocked.Clear();
foreach (var a in achievements)
{
if (a.unlocked)
previouslyUnlocked.Add(a.id);
}
// Update achievements
achievements.Clear();
if (response.achievements != null)
{
achievements.AddRange(response.achievements);
}
onComplete?.Invoke(response);
}
else
{
Debug.LogWarning($"Failed to load achievements: {request.error}");
onComplete?.Invoke(null);
}
}
}
// ==============================================
// Check for New Achievements
// ==============================================
public void CheckForNewUnlocks()
{
foreach (var achievement in achievements)
{
if (achievement.unlocked && !previouslyUnlocked.Contains(achievement.id))
{
// New unlock!
ShowAchievementPopup(achievement);
OnAchievementUnlocked?.Invoke(achievement);
}
}
}
// Call after each game session to check for new achievements
public IEnumerator RefreshAndCheckAchievements(string userId)
{
yield return LoadAchievements(userId, (response) =>
{
if (response != null)
{
CheckForNewUnlocks();
}
});
}
// ==============================================
// Achievement Popup
// ==============================================
public void ShowAchievementPopup(Achievement achievement)
{
popupQueue.Enqueue(achievement);
if (!isShowingPopup)
{
StartCoroutine(ProcessPopupQueue());
}
}
private IEnumerator ProcessPopupQueue()
{
isShowingPopup = true;
while (popupQueue.Count > 0)
{
var achievement = popupQueue.Dequeue();
yield return ShowSinglePopup(achievement);
yield return new WaitForSeconds(0.3f); // Gap between popups
}
isShowingPopup = false;
}
private IEnumerator ShowSinglePopup(Achievement achievement)
{
Debug.Log($"[Achievement] Unlocked: {achievement.name}");
// Play sound
if (unlockSound != null && AudioManager.Instance != null)
{
AudioManager.Instance.PlaySFX(unlockSound);
}
// Create popup UI
if (achievementPopupPrefab != null && popupContainer != null)
{
var popup = Instantiate(achievementPopupPrefab, popupContainer);
var popupScript = popup.GetComponent<AchievementPopup>();
if (popupScript != null)
{
popupScript.Setup(achievement);
}
yield return new WaitForSeconds(popupDuration);
if (popup != null)
{
Destroy(popup);
}
}
else
{
// Fallback: just wait
yield return new WaitForSeconds(popupDuration);
}
}
// ==============================================
// Public API
// ==============================================
public List<Achievement> GetAllAchievements()
{
return new List<Achievement>(achievements);
}
public List<Achievement> GetUnlockedAchievements()
{
return achievements.FindAll(a => a.unlocked);
}
public List<Achievement> GetLockedAchievements()
{
return achievements.FindAll(a => !a.unlocked);
}
public List<Achievement> GetAchievementsByCategory(string category)
{
return achievements.FindAll(a => a.category == category);
}
public int GetUnlockedCount()
{
int count = 0;
foreach (var a in achievements)
{
if (a.unlocked) count++;
}
return count;
}
public int GetTotalCount()
{
return achievements.Count;
}
public float GetCompletionPercentage()
{
if (achievements.Count == 0) return 0f;
return (float)GetUnlockedCount() / achievements.Count * 100f;
}
public Achievement GetAchievement(string id)
{
return achievements.Find(a => a.id == id);
}
// ==============================================
// Icon Mapping
// ==============================================
public Sprite GetIconSprite(string iconName)
{
// Load from Resources/Icons/Achievements/
string path = $"Icons/Achievements/{iconName}";
var sprite = Resources.Load<Sprite>(path);
if (sprite == null)
{
// Fallback icon
sprite = Resources.Load<Sprite>("Icons/Achievements/default");
}
return sprite;
}
// ==============================================
// Helpers
// ==============================================
private string GetBaseUrl()
{
if (BreakpilotAPI.Instance != null)
{
return "http://localhost:8000/api/game"; // TODO: Get from BreakpilotAPI
}
return "http://localhost:8000/api/game";
}
}
// ==============================================
// Achievement Popup Component
// ==============================================
public class AchievementPopup : MonoBehaviour
{
[SerializeField] private UnityEngine.UI.Text titleText;
[SerializeField] private UnityEngine.UI.Text descriptionText;
[SerializeField] private UnityEngine.UI.Image iconImage;
public void Setup(Achievement achievement)
{
if (titleText != null)
titleText.text = achievement.name;
if (descriptionText != null)
descriptionText.text = achievement.description;
if (iconImage != null)
{
var sprite = AchievementManager.Instance?.GetIconSprite(achievement.icon);
if (sprite != null)
iconImage.sprite = sprite;
}
// Animate in
StartCoroutine(AnimateIn());
}
private IEnumerator AnimateIn()
{
var canvasGroup = GetComponent<CanvasGroup>();
if (canvasGroup != null)
{
canvasGroup.alpha = 0f;
float duration = 0.3f;
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
canvasGroup.alpha = Mathf.Lerp(0f, 1f, elapsed / duration);
yield return null;
}
canvasGroup.alpha = 1f;
}
}
}
}

View File

@@ -0,0 +1,290 @@
// ==============================================
// AudioManager.cs - Audio-Verwaltung
// ==============================================
// Verwaltet Musik, Sound-Effekte und
// Text-to-Speech fuer das Spiel.
using System.Collections.Generic;
using UnityEngine;
namespace BreakpilotDrive
{
[System.Serializable]
public class SoundEffect
{
public string name;
public AudioClip clip;
[Range(0f, 1f)]
public float volume = 1f;
[Range(0.5f, 1.5f)]
public float pitch = 1f;
public bool loop = false;
}
public class AudioManager : MonoBehaviour
{
public static AudioManager Instance { get; private set; }
[Header("Audio Sources")]
[SerializeField] private AudioSource musicSource;
[SerializeField] private AudioSource sfxSource;
[SerializeField] private AudioSource voiceSource;
[Header("Musik")]
[SerializeField] private AudioClip menuMusic;
[SerializeField] private AudioClip gameMusic;
[Header("Sound-Effekte")]
[SerializeField] private SoundEffect[] soundEffects;
[Header("Lautstaerke")]
[Range(0f, 1f)]
[SerializeField] private float masterVolume = 1f;
[Range(0f, 1f)]
[SerializeField] private float musicVolume = 0.5f;
[Range(0f, 1f)]
[SerializeField] private float sfxVolume = 1f;
[Range(0f, 1f)]
[SerializeField] private float voiceVolume = 1f;
// Sound-Dictionary fuer schnellen Zugriff
private Dictionary<string, SoundEffect> soundDict = new Dictionary<string, SoundEffect>();
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
InitializeSounds();
LoadVolumeSettings();
}
else
{
Destroy(gameObject);
}
}
private void InitializeSounds()
{
soundDict.Clear();
foreach (var sound in soundEffects)
{
if (!string.IsNullOrEmpty(sound.name) && sound.clip != null)
{
soundDict[sound.name.ToLower()] = sound;
}
}
}
// ==============================================
// Musik
// ==============================================
public void PlayMenuMusic()
{
PlayMusic(menuMusic);
}
public void PlayGameMusic()
{
PlayMusic(gameMusic);
}
public void PlayMusic(AudioClip clip)
{
if (musicSource == null || clip == null) return;
if (musicSource.clip == clip && musicSource.isPlaying)
return;
musicSource.clip = clip;
musicSource.volume = musicVolume * masterVolume;
musicSource.loop = true;
musicSource.Play();
}
public void StopMusic()
{
if (musicSource != null)
{
musicSource.Stop();
}
}
public void PauseMusic()
{
if (musicSource != null)
{
musicSource.Pause();
}
}
public void ResumeMusic()
{
if (musicSource != null)
{
musicSource.UnPause();
}
}
// ==============================================
// Sound-Effekte
// ==============================================
public void PlaySound(string soundName)
{
if (sfxSource == null) return;
string key = soundName.ToLower();
if (soundDict.TryGetValue(key, out SoundEffect sound))
{
sfxSource.pitch = sound.pitch;
sfxSource.PlayOneShot(sound.clip, sound.volume * sfxVolume * masterVolume);
}
else
{
Debug.LogWarning($"Sound '{soundName}' nicht gefunden!");
}
}
public void PlaySound(AudioClip clip, float volume = 1f)
{
if (sfxSource == null || clip == null) return;
sfxSource.PlayOneShot(clip, volume * sfxVolume * masterVolume);
}
// Vordefinierte Sound-Methoden
public void PlayCoinSound() => PlaySound("coin");
public void PlayCrashSound() => PlaySound("crash");
public void PlayCorrectSound() => PlaySound("correct");
public void PlayWrongSound() => PlaySound("wrong");
public void PlayButtonSound() => PlaySound("button");
// ==============================================
// Text-to-Speech (WebGL)
// ==============================================
public void Speak(string text)
{
if (string.IsNullOrEmpty(text)) return;
#if UNITY_WEBGL && !UNITY_EDITOR
SpeakWebGL(text);
#else
Debug.Log($"[TTS] {text}");
#endif
}
public void SpeakQuestion(QuizQuestion question)
{
if (question == null) return;
Speak(question.question_text);
// Optionen vorlesen (mit Verzoegerung)
for (int i = 0; i < question.options.Length; i++)
{
string optionText = $"Option {i + 1}: {question.options[i]}";
// In WebGL: Verzoegerung ueber JavaScript
#if UNITY_WEBGL && !UNITY_EDITOR
SpeakDelayedWebGL(optionText, 1.5f + (i * 1.5f));
#else
Debug.Log($"[TTS] {optionText}");
#endif
}
}
public void SpeakFeedback(bool correct)
{
string message = correct ?
"Richtig! Gut gemacht!" :
"Nicht ganz. Versuch es nochmal!";
Speak(message);
}
public void StopSpeaking()
{
#if UNITY_WEBGL && !UNITY_EDITOR
StopSpeakingWebGL();
#endif
}
// WebGL JavaScript Interop
#if UNITY_WEBGL && !UNITY_EDITOR
[System.Runtime.InteropServices.DllImport("__Internal")]
private static extern void SpeakWebGL(string text);
[System.Runtime.InteropServices.DllImport("__Internal")]
private static extern void SpeakDelayedWebGL(string text, float delaySeconds);
[System.Runtime.InteropServices.DllImport("__Internal")]
private static extern void StopSpeakingWebGL();
#endif
// ==============================================
// Lautstaerke-Einstellungen
// ==============================================
public void SetMasterVolume(float volume)
{
masterVolume = Mathf.Clamp01(volume);
UpdateAllVolumes();
SaveVolumeSettings();
}
public void SetMusicVolume(float volume)
{
musicVolume = Mathf.Clamp01(volume);
if (musicSource != null)
{
musicSource.volume = musicVolume * masterVolume;
}
SaveVolumeSettings();
}
public void SetSFXVolume(float volume)
{
sfxVolume = Mathf.Clamp01(volume);
SaveVolumeSettings();
}
public void SetVoiceVolume(float volume)
{
voiceVolume = Mathf.Clamp01(volume);
if (voiceSource != null)
{
voiceSource.volume = voiceVolume * masterVolume;
}
SaveVolumeSettings();
}
private void UpdateAllVolumes()
{
if (musicSource != null)
musicSource.volume = musicVolume * masterVolume;
if (voiceSource != null)
voiceSource.volume = voiceVolume * masterVolume;
}
private void SaveVolumeSettings()
{
PlayerPrefs.SetFloat("MasterVolume", masterVolume);
PlayerPrefs.SetFloat("MusicVolume", musicVolume);
PlayerPrefs.SetFloat("SFXVolume", sfxVolume);
PlayerPrefs.SetFloat("VoiceVolume", voiceVolume);
PlayerPrefs.Save();
}
private void LoadVolumeSettings()
{
masterVolume = PlayerPrefs.GetFloat("MasterVolume", 1f);
musicVolume = PlayerPrefs.GetFloat("MusicVolume", 0.5f);
sfxVolume = PlayerPrefs.GetFloat("SFXVolume", 1f);
voiceVolume = PlayerPrefs.GetFloat("VoiceVolume", 1f);
UpdateAllVolumes();
}
// Properties fuer UI-Sliders
public float MasterVolume => masterVolume;
public float MusicVolume => musicVolume;
public float SFXVolume => sfxVolume;
public float VoiceVolume => voiceVolume;
}
}

View File

@@ -0,0 +1,247 @@
// ==============================================
// AuthManager.cs - Unity Auth Handler
// ==============================================
// Handles JWT token for Keycloak authentication.
// Supports token via URL parameter or PostMessage from parent frame.
using System;
using System.Runtime.InteropServices;
using UnityEngine;
namespace BreakpilotDrive
{
public class AuthManager : MonoBehaviour
{
public static AuthManager Instance { get; private set; }
[Header("Auth Configuration")]
[SerializeField] private bool requireAuth = false;
[SerializeField] private string devUserId = "dev-user-123";
// Current auth state
private string jwtToken;
private string userId;
private bool isAuthenticated;
// Events
public event Action<string> OnAuthenticated;
public event Action<string> OnAuthFailed;
// JavaScript interface for WebGL
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern string GetTokenFromURL();
[DllImport("__Internal")]
private static extern string GetTokenFromParent();
[DllImport("__Internal")]
private static extern void RequestTokenFromParent();
#endif
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
void Start()
{
InitializeAuth();
}
// ==============================================
// Initialization
// ==============================================
private void InitializeAuth()
{
// Development mode - use dev user
if (!requireAuth || Application.isEditor)
{
SetDevUser();
return;
}
#if UNITY_WEBGL && !UNITY_EDITOR
// Try URL parameter first
string urlToken = GetTokenFromURL();
if (!string.IsNullOrEmpty(urlToken))
{
SetToken(urlToken);
return;
}
// Try parent frame (iframe scenario)
RequestTokenFromParent();
#else
// Non-WebGL builds use dev user
SetDevUser();
#endif
}
private void SetDevUser()
{
userId = devUserId;
isAuthenticated = true;
jwtToken = null;
Debug.Log($"[Auth] Development mode - User: {userId}");
OnAuthenticated?.Invoke(userId);
}
// ==============================================
// Token Management
// ==============================================
public void SetToken(string token)
{
if (string.IsNullOrEmpty(token))
{
OnAuthFailed?.Invoke("Empty token");
return;
}
jwtToken = token;
userId = ExtractUserIdFromToken(token);
if (!string.IsNullOrEmpty(userId))
{
isAuthenticated = true;
Debug.Log($"[Auth] Authenticated - User: {userId}");
OnAuthenticated?.Invoke(userId);
}
else
{
OnAuthFailed?.Invoke("Could not extract user ID from token");
}
}
// Called from JavaScript via SendMessage
public void ReceiveTokenFromJS(string token)
{
Debug.Log("[Auth] Received token from JavaScript");
SetToken(token);
}
// Called from JavaScript if auth fails
public void AuthFailedFromJS(string error)
{
Debug.LogWarning($"[Auth] JavaScript auth failed: {error}");
// Fall back to dev user in development
if (!requireAuth)
{
SetDevUser();
}
else
{
OnAuthFailed?.Invoke(error);
}
}
private string ExtractUserIdFromToken(string token)
{
try
{
// JWT has 3 parts: header.payload.signature
string[] parts = token.Split('.');
if (parts.Length != 3) return null;
// Decode payload (base64)
string payload = parts[1];
// Fix base64 padding
int padding = 4 - (payload.Length % 4);
if (padding < 4) payload += new string('=', padding);
// Replace URL-safe chars
payload = payload.Replace('-', '+').Replace('_', '/');
byte[] bytes = Convert.FromBase64String(payload);
string json = System.Text.Encoding.UTF8.GetString(bytes);
// Simple JSON parsing for "sub" claim
var claims = JsonUtility.FromJson<JWTPayload>(json);
return claims?.sub;
}
catch (Exception e)
{
Debug.LogWarning($"[Auth] Token parsing failed: {e.Message}");
return null;
}
}
[Serializable]
private class JWTPayload
{
public string sub;
public string email;
public string name;
public long exp;
public long iat;
}
// ==============================================
// Public API
// ==============================================
public bool IsAuthenticated => isAuthenticated;
public string UserId => userId;
public string Token => jwtToken;
public bool RequiresAuth => requireAuth;
public string GetAuthHeader()
{
if (string.IsNullOrEmpty(jwtToken))
return null;
return $"Bearer {jwtToken}";
}
public void Logout()
{
jwtToken = null;
userId = null;
isAuthenticated = false;
Debug.Log("[Auth] Logged out");
}
// Check if token is expired
public bool IsTokenExpired()
{
if (string.IsNullOrEmpty(jwtToken))
return true;
try
{
string[] parts = jwtToken.Split('.');
if (parts.Length != 3) return true;
string payload = parts[1];
int padding = 4 - (payload.Length % 4);
if (padding < 4) payload += new string('=', padding);
payload = payload.Replace('-', '+').Replace('_', '/');
byte[] bytes = Convert.FromBase64String(payload);
string json = System.Text.Encoding.UTF8.GetString(bytes);
var claims = JsonUtility.FromJson<JWTPayload>(json);
if (claims?.exp > 0)
{
var expTime = DateTimeOffset.FromUnixTimeSeconds(claims.exp).UtcDateTime;
return DateTime.UtcNow > expTime;
}
}
catch { }
return false;
}
}
}

View File

@@ -0,0 +1,292 @@
// ==============================================
// DifficultyManager.cs - Schwierigkeits-Steuerung
// ==============================================
// Passt die Spielschwierigkeit dynamisch an
// basierend auf Lernniveau und Spielerleistung.
using System;
using UnityEngine;
namespace BreakpilotDrive
{
public class DifficultyManager : MonoBehaviour
{
public static DifficultyManager Instance { get; private set; }
[Header("Schwierigkeits-Bereich")]
[SerializeField] private int minDifficulty = 1;
[SerializeField] private int maxDifficulty = 5;
[Header("Dynamische Anpassung")]
[SerializeField] private bool enableDynamicDifficulty = true;
[SerializeField] private int questionsForEvaluation = 5;
[SerializeField] private float accuracyToIncrease = 0.8f; // 80% richtig = schwerer
[SerializeField] private float accuracyToDecrease = 0.4f; // 40% richtig = leichter
[Header("Geschwindigkeits-Steigerung")]
[SerializeField] private bool enableSpeedIncrease = true;
[SerializeField] private float speedIncreaseInterval = 30f; // Alle X Sekunden
[SerializeField] private float speedIncreaseAmount = 0.5f; // Um X erhoehen
// Aktueller Zustand
private int currentDifficulty = 3;
private GameDifficulty currentSettings;
private float timeSinceLastSpeedIncrease = 0f;
// Statistik fuer dynamische Anpassung
private int recentQuestionsAnswered = 0;
private int recentQuestionsCorrect = 0;
// Events
public event Action<int> OnDifficultyChanged;
public event Action<GameDifficulty> OnSettingsUpdated;
// Properties
public int CurrentDifficulty => currentDifficulty;
public GameDifficulty CurrentSettings => currentSettings;
void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
void Start()
{
// Quiz-Events abonnieren
if (QuizManager.Instance != null)
{
QuizManager.Instance.OnQuestionAnswered += OnQuestionAnswered;
}
// Initiale Schwierigkeit laden
LoadDifficulty(currentDifficulty);
}
void OnDestroy()
{
if (QuizManager.Instance != null)
{
QuizManager.Instance.OnQuestionAnswered -= OnQuestionAnswered;
}
}
void Update()
{
if (GameManager.Instance?.CurrentState != GameState.Playing)
return;
// Geschwindigkeit graduell erhoehen
if (enableSpeedIncrease)
{
timeSinceLastSpeedIncrease += Time.deltaTime;
if (timeSinceLastSpeedIncrease >= speedIncreaseInterval)
{
timeSinceLastSpeedIncrease = 0f;
IncreaseSpeed();
}
}
}
// ==============================================
// Schwierigkeit laden
// ==============================================
public void LoadDifficulty(int level)
{
currentDifficulty = Mathf.Clamp(level, minDifficulty, maxDifficulty);
if (BreakpilotAPI.Instance != null)
{
StartCoroutine(BreakpilotAPI.Instance.GetDifficulty(currentDifficulty,
onSuccess: (settings) =>
{
currentSettings = settings;
ApplySettings(settings);
OnSettingsUpdated?.Invoke(settings);
Debug.Log($"Schwierigkeit {currentDifficulty} geladen");
},
onError: (error) =>
{
Debug.LogWarning($"Konnte Schwierigkeit nicht laden: {error}");
// Fallback zu Default-Werten
currentSettings = GetDefaultSettings(currentDifficulty);
ApplySettings(currentSettings);
}
));
}
else
{
currentSettings = GetDefaultSettings(currentDifficulty);
ApplySettings(currentSettings);
}
OnDifficultyChanged?.Invoke(currentDifficulty);
}
private void ApplySettings(GameDifficulty settings)
{
// Geschwindigkeit anwenden
if (TrackGenerator.Instance != null)
{
TrackGenerator.Instance.SetSpeed(settings.lane_speed);
}
if (PlayerController.Instance != null)
{
PlayerController.Instance.SetSpeed(settings.lane_speed);
}
// Hindernis-Frequenz anwenden
// ObstacleSpawner liest die Settings direkt aus GameManager
Debug.Log($"Settings angewendet: Speed={settings.lane_speed}, " +
$"Obstacles={settings.obstacle_frequency}, " +
$"Hints={settings.hints_enabled}");
}
// ==============================================
// Dynamische Anpassung
// ==============================================
private void OnQuestionAnswered(bool correct, int points)
{
if (!enableDynamicDifficulty) return;
recentQuestionsAnswered++;
if (correct) recentQuestionsCorrect++;
// Evaluation nach X Fragen
if (recentQuestionsAnswered >= questionsForEvaluation)
{
EvaluateAndAdjust();
}
}
private void EvaluateAndAdjust()
{
float accuracy = (float)recentQuestionsCorrect / recentQuestionsAnswered;
Debug.Log($"Quiz-Evaluation: {recentQuestionsCorrect}/{recentQuestionsAnswered} " +
$"= {accuracy:P0}");
if (accuracy >= accuracyToIncrease && currentDifficulty < maxDifficulty)
{
// Spieler ist gut - Schwierigkeit erhoehen
IncreaseDifficulty();
}
else if (accuracy <= accuracyToDecrease && currentDifficulty > minDifficulty)
{
// Spieler hat Probleme - Schwierigkeit verringern
DecreaseDifficulty();
}
// Reset fuer naechste Evaluation
recentQuestionsAnswered = 0;
recentQuestionsCorrect = 0;
}
public void IncreaseDifficulty()
{
if (currentDifficulty < maxDifficulty)
{
LoadDifficulty(currentDifficulty + 1);
Debug.Log($"Schwierigkeit erhoeht auf {currentDifficulty}");
}
}
public void DecreaseDifficulty()
{
if (currentDifficulty > minDifficulty)
{
LoadDifficulty(currentDifficulty - 1);
Debug.Log($"Schwierigkeit verringert auf {currentDifficulty}");
}
}
// ==============================================
// Geschwindigkeits-Steigerung
// ==============================================
private void IncreaseSpeed()
{
if (currentSettings == null) return;
float newSpeed = currentSettings.lane_speed + speedIncreaseAmount;
// Maximal-Geschwindigkeit basierend auf Difficulty
float maxSpeed = 5f + (currentDifficulty * 2f);
newSpeed = Mathf.Min(newSpeed, maxSpeed);
currentSettings.lane_speed = newSpeed;
if (TrackGenerator.Instance != null)
{
TrackGenerator.Instance.SetSpeed(newSpeed);
}
Debug.Log($"Geschwindigkeit erhoeht auf {newSpeed:F1}");
}
// ==============================================
// Default-Werte (Fallback)
// ==============================================
private GameDifficulty GetDefaultSettings(int level)
{
return new GameDifficulty
{
lane_speed = 4f + level,
obstacle_frequency = 0.3f + (level * 0.1f),
power_up_chance = 0.4f - (level * 0.05f),
question_complexity = level,
answer_time = 15 - (level * 2),
hints_enabled = level <= 2,
speech_speed = 0.8f + (level * 0.1f)
};
}
// ==============================================
// Oeffentliche Methoden
// ==============================================
public void SetDifficulty(int level)
{
LoadDifficulty(level);
}
public void ResetForNewGame()
{
recentQuestionsAnswered = 0;
recentQuestionsCorrect = 0;
timeSinceLastSpeedIncrease = 0f;
// Schwierigkeit von User-Level laden
if (BreakpilotAPI.Instance != null)
{
var cachedLevel = BreakpilotAPI.Instance.GetCachedLevel();
if (cachedLevel != null)
{
LoadDifficulty(cachedLevel.overall_level);
}
}
}
public float GetCurrentSpeed()
{
return currentSettings?.lane_speed ?? 5f;
}
public bool AreHintsEnabled()
{
return currentSettings?.hints_enabled ?? false;
}
public int GetAnswerTime()
{
return currentSettings?.answer_time ?? 10;
}
}
}

View File

@@ -0,0 +1,317 @@
// ==============================================
// GameManager.cs - Zentrale Spielsteuerung
// ==============================================
// Verwaltet Spielzustand, Score, Leben und
// koordiniert alle anderen Manager.
using System;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace BreakpilotDrive
{
public enum GameState
{
MainMenu,
Playing,
Paused,
QuizActive,
GameOver
}
public class GameManager : MonoBehaviour
{
public static GameManager Instance { get; private set; }
[Header("Spieleinstellungen")]
[SerializeField] private int startLives = 3;
[SerializeField] private float pauseQuestionInterval = 500f; // Alle X Meter
[Header("UI Referenzen")]
[SerializeField] private GameObject gameOverPanel;
[SerializeField] private GameObject pausePanel;
// Spielzustand
private GameState currentState = GameState.MainMenu;
private int score = 0;
private int lives;
private float distanceTraveled = 0f;
private float playTime = 0f;
private float nextPauseQuestionDistance;
// Schwierigkeit (von API geladen)
private int currentDifficulty = 3;
private GameDifficulty difficultySettings;
// Events
public event Action<int> OnScoreChanged;
public event Action<int> OnLivesChanged;
public event Action<float> OnDistanceChanged;
public event Action<GameState> OnStateChanged;
// Properties
public GameState CurrentState => currentState;
public int Score => score;
public int Lives => lives;
public float DistanceTraveled => distanceTraveled;
public float PlayTime => playTime;
public int CurrentDifficulty => currentDifficulty;
public GameDifficulty DifficultySettings => difficultySettings;
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
void Start()
{
// UI initial verstecken
if (gameOverPanel) gameOverPanel.SetActive(false);
if (pausePanel) pausePanel.SetActive(false);
}
void Update()
{
if (currentState == GameState.Playing)
{
// Spielzeit zaehlen
playTime += Time.deltaTime;
// Pause-Fragen pruefen
if (distanceTraveled >= nextPauseQuestionDistance)
{
TriggerPauseQuestion();
}
// Escape fuer Pause
if (Input.GetKeyDown(KeyCode.Escape))
{
PauseGame();
}
}
else if (currentState == GameState.Paused)
{
if (Input.GetKeyDown(KeyCode.Escape))
{
ResumeGame();
}
}
}
// ==============================================
// Spiel starten
// ==============================================
public void StartGame(string userId = "guest")
{
// Werte zuruecksetzen
score = 0;
lives = startLives;
distanceTraveled = 0f;
playTime = 0f;
nextPauseQuestionDistance = pauseQuestionInterval;
// Events ausloesen
OnScoreChanged?.Invoke(score);
OnLivesChanged?.Invoke(lives);
// Schwierigkeit laden
if (BreakpilotAPI.Instance != null)
{
StartCoroutine(BreakpilotAPI.Instance.GetLearningLevel(userId,
onSuccess: (level) =>
{
currentDifficulty = level.overall_level;
LoadDifficultySettings();
}
));
}
else
{
LoadDifficultySettings();
}
// Quiz-Statistik zuruecksetzen
if (QuizManager.Instance != null)
{
QuizManager.Instance.ResetStatistics();
}
SetState(GameState.Playing);
}
private void LoadDifficultySettings()
{
if (BreakpilotAPI.Instance != null)
{
StartCoroutine(BreakpilotAPI.Instance.GetDifficulty(currentDifficulty,
onSuccess: (settings) =>
{
difficultySettings = settings;
Debug.Log($"Schwierigkeit {currentDifficulty} geladen: Speed={settings.lane_speed}");
}
));
}
}
// ==============================================
// Score-System
// ==============================================
public void AddScore(int points)
{
score = Mathf.Max(0, score + points);
OnScoreChanged?.Invoke(score);
}
public void AddDistance(float distance)
{
distanceTraveled += distance;
OnDistanceChanged?.Invoke(distanceTraveled);
}
// ==============================================
// Leben-System
// ==============================================
public void LoseLife()
{
lives--;
OnLivesChanged?.Invoke(lives);
if (lives <= 0)
{
GameOver();
}
}
public void GainLife()
{
lives++;
OnLivesChanged?.Invoke(lives);
}
// ==============================================
// Pause-Fragen
// ==============================================
private void TriggerPauseQuestion()
{
nextPauseQuestionDistance += pauseQuestionInterval;
if (BreakpilotAPI.Instance != null && QuizManager.Instance != null)
{
// Pause-Frage aus Cache holen
var questions = BreakpilotAPI.Instance.GetCachedQuestions();
var pauseQuestions = questions.FindAll(q => q.quiz_mode == "pause");
if (pauseQuestions.Count > 0)
{
int index = UnityEngine.Random.Range(0, pauseQuestions.Count);
QuizManager.Instance.ShowPauseQuestion(pauseQuestions[index]);
}
}
}
// ==============================================
// Spielzustand
// ==============================================
public void SetState(GameState newState)
{
currentState = newState;
OnStateChanged?.Invoke(newState);
switch (newState)
{
case GameState.Playing:
Time.timeScale = 1f;
break;
case GameState.Paused:
case GameState.QuizActive:
Time.timeScale = 0f;
break;
case GameState.GameOver:
Time.timeScale = 0f;
break;
}
}
public void PauseGame()
{
if (currentState == GameState.Playing)
{
SetState(GameState.Paused);
if (pausePanel) pausePanel.SetActive(true);
}
}
public void ResumeGame()
{
if (currentState == GameState.Paused)
{
SetState(GameState.Playing);
if (pausePanel) pausePanel.SetActive(false);
}
}
// ==============================================
// Spiel beenden
// ==============================================
public void GameOver()
{
SetState(GameState.GameOver);
if (gameOverPanel) gameOverPanel.SetActive(true);
// Session speichern
SaveSession();
}
private void SaveSession()
{
if (BreakpilotAPI.Instance == null) return;
GameSession session = new GameSession
{
user_id = "guest", // TODO: Echte User-ID
game_mode = "video",
duration_seconds = Mathf.RoundToInt(playTime),
distance_traveled = distanceTraveled,
score = score,
questions_answered = QuizManager.Instance?.GetQuestionsAnswered() ?? 0,
questions_correct = QuizManager.Instance?.GetQuestionsCorrect() ?? 0,
difficulty_level = currentDifficulty
};
StartCoroutine(BreakpilotAPI.Instance.SaveGameSession(session,
onSuccess: (response) =>
{
Debug.Log($"Session gespeichert: {response.session_id}");
if (response.new_level.HasValue)
{
Debug.Log($"Level Up! Neues Level: {response.new_level}");
}
}
));
}
// ==============================================
// Neustart
// ==============================================
public void RestartGame()
{
if (gameOverPanel) gameOverPanel.SetActive(false);
StartGame();
}
public void LoadMainMenu()
{
Time.timeScale = 1f;
SceneManager.LoadScene("MainMenu");
}
}
}