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:
348
breakpilot-drive/UnityScripts/Core/AchievementManager.cs
Normal file
348
breakpilot-drive/UnityScripts/Core/AchievementManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
290
breakpilot-drive/UnityScripts/Core/AudioManager.cs
Normal file
290
breakpilot-drive/UnityScripts/Core/AudioManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
247
breakpilot-drive/UnityScripts/Core/AuthManager.cs
Normal file
247
breakpilot-drive/UnityScripts/Core/AuthManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
292
breakpilot-drive/UnityScripts/Core/DifficultyManager.cs
Normal file
292
breakpilot-drive/UnityScripts/Core/DifficultyManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
317
breakpilot-drive/UnityScripts/Core/GameManager.cs
Normal file
317
breakpilot-drive/UnityScripts/Core/GameManager.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user