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 21a844cb8a
1986 changed files with 744143 additions and 1731 deletions

View File

@@ -0,0 +1,599 @@
// ==============================================
// BreakpilotAPI.cs - Unity API Client
// ==============================================
// Kopiere diese Datei nach Assets/Scripts/Network/
// um mit dem Breakpilot Backend zu kommunizieren.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
namespace BreakpilotDrive
{
[Serializable]
public class LearningLevel
{
public string user_id;
public int overall_level;
public float math_level;
public float german_level;
public float english_level;
}
[Serializable]
public class GameDifficulty
{
public float lane_speed;
public float obstacle_frequency;
public float power_up_chance;
public int question_complexity;
public int answer_time;
public bool hints_enabled;
public float speech_speed;
}
[Serializable]
public class QuizQuestion
{
public string id;
public string question_text;
public string audio_url;
public string[] options;
public int correct_index;
public int difficulty;
public string subject;
public int grade_level;
public string quiz_mode; // "quick" oder "pause"
public string visual_trigger; // z.B. "bridge"
public float time_limit_seconds;
}
[Serializable]
public class QuizQuestionList
{
public QuizQuestion[] questions;
}
[Serializable]
public class GameSession
{
public string user_id;
public string game_mode; // "video" oder "audio"
public int duration_seconds;
public float distance_traveled;
public int score;
public int questions_answered;
public int questions_correct;
public int difficulty_level;
}
[Serializable]
public class SessionResponse
{
public string session_id;
public string status;
public int? new_level;
}
[Serializable]
public class VisualTrigger
{
public string trigger;
public int question_count;
public int[] difficulties;
public string[] subjects;
}
[Serializable]
public class ProgressData
{
public string date;
public int sessions;
public int total_score;
public int questions;
public int correct;
public float accuracy;
public float avg_difficulty;
}
[Serializable]
public class ProgressResponse
{
public string user_id;
public int days;
public int data_points;
public ProgressData[] progress;
}
[Serializable]
public class LeaderboardEntry
{
public int rank;
public string user_id;
public int total_score;
public string display_name;
}
public class BreakpilotAPI : MonoBehaviour
{
// Singleton Pattern
public static BreakpilotAPI Instance { get; private set; }
[Header("API Konfiguration")]
[SerializeField] private string baseUrl = "http://localhost:8000/api/game";
[SerializeField] private bool useOfflineCache = true;
// Cached Data
private LearningLevel cachedLevel;
private GameDifficulty cachedDifficulty;
private List<QuizQuestion> cachedQuestions = new List<QuizQuestion>();
private List<VisualTrigger> cachedTriggers = new List<VisualTrigger>();
// API URL property (for other scripts)
public string BaseUrl => baseUrl;
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
// ==============================================
// Helper: Add Auth Header to Request
// ==============================================
private void AddAuthHeader(UnityWebRequest request)
{
if (AuthManager.Instance != null)
{
string authHeader = AuthManager.Instance.GetAuthHeader();
if (!string.IsNullOrEmpty(authHeader))
{
request.SetRequestHeader("Authorization", authHeader);
}
}
}
// Get current user ID (from auth or fallback)
public string GetCurrentUserId()
{
if (AuthManager.Instance != null && AuthManager.Instance.IsAuthenticated)
{
return AuthManager.Instance.UserId;
}
return "anonymous";
}
// ==============================================
// Lernniveau abrufen
// ==============================================
public IEnumerator GetLearningLevel(string userId, Action<LearningLevel> onSuccess, Action<string> onError = null)
{
string url = $"{baseUrl}/learning-level/{userId}";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
LearningLevel level = JsonUtility.FromJson<LearningLevel>(request.downloadHandler.text);
cachedLevel = level;
onSuccess?.Invoke(level);
}
else
{
Debug.LogWarning($"API Fehler: {request.error}");
// Offline-Fallback
if (useOfflineCache && cachedLevel != null)
{
onSuccess?.Invoke(cachedLevel);
}
else
{
// Standard-Level
onSuccess?.Invoke(new LearningLevel { user_id = userId, overall_level = 3 });
}
onError?.Invoke(request.error);
}
}
}
// ==============================================
// Schwierigkeit abrufen
// ==============================================
public IEnumerator GetDifficulty(int level, Action<GameDifficulty> onSuccess, Action<string> onError = null)
{
string url = $"{baseUrl}/difficulty/{level}";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
GameDifficulty difficulty = JsonUtility.FromJson<GameDifficulty>(request.downloadHandler.text);
cachedDifficulty = difficulty;
onSuccess?.Invoke(difficulty);
}
else
{
Debug.LogWarning($"API Fehler: {request.error}");
// Fallback: Default-Schwierigkeit
if (cachedDifficulty != null)
{
onSuccess?.Invoke(cachedDifficulty);
}
else
{
onSuccess?.Invoke(GetDefaultDifficulty(level));
}
onError?.Invoke(request.error);
}
}
}
// ==============================================
// Quiz-Fragen abrufen
// ==============================================
public IEnumerator GetQuizQuestions(
int difficulty,
int count,
string mode = null, // "quick", "pause", oder null fuer beide
string subject = null,
Action<QuizQuestion[]> onSuccess = null,
Action<string> onError = null)
{
string url = $"{baseUrl}/quiz/questions?difficulty={difficulty}&count={count}";
if (!string.IsNullOrEmpty(mode)) url += $"&mode={mode}";
if (!string.IsNullOrEmpty(subject)) url += $"&subject={subject}";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
// Unity's JsonUtility braucht Wrapper fuer Arrays
string wrappedJson = "{\"questions\":" + request.downloadHandler.text + "}";
QuizQuestionList list = JsonUtility.FromJson<QuizQuestionList>(wrappedJson);
// Cache updaten
if (list.questions != null)
{
cachedQuestions.AddRange(list.questions);
}
onSuccess?.Invoke(list.questions);
}
else
{
Debug.LogWarning($"API Fehler: {request.error}");
// Offline-Fallback
if (useOfflineCache && cachedQuestions.Count > 0)
{
var filtered = cachedQuestions.FindAll(q =>
Math.Abs(q.difficulty - difficulty) <= 1 &&
(string.IsNullOrEmpty(mode) || q.quiz_mode == mode)
);
onSuccess?.Invoke(filtered.GetRange(0, Math.Min(count, filtered.Count)).ToArray());
}
onError?.Invoke(request.error);
}
}
}
// ==============================================
// Visuelle Trigger abrufen
// ==============================================
public IEnumerator GetVisualTriggers(Action<VisualTrigger[]> onSuccess, Action<string> onError = null)
{
string url = $"{baseUrl}/quiz/visual-triggers";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
// Parse Array
string wrappedJson = "{\"triggers\":" + request.downloadHandler.text + "}";
VisualTriggersWrapper wrapper = JsonUtility.FromJson<VisualTriggersWrapper>(wrappedJson);
cachedTriggers = new List<VisualTrigger>(wrapper.triggers);
onSuccess?.Invoke(wrapper.triggers);
}
else
{
Debug.LogWarning($"API Fehler: {request.error}");
onError?.Invoke(request.error);
}
}
}
[Serializable]
private class VisualTriggersWrapper
{
public VisualTrigger[] triggers;
}
// ==============================================
// Spielsession speichern
// ==============================================
public IEnumerator SaveGameSession(GameSession session, Action<SessionResponse> onSuccess = null, Action<string> onError = null)
{
string url = $"{baseUrl}/session";
string json = JsonUtility.ToJson(session);
using (UnityWebRequest request = new UnityWebRequest(url, "POST"))
{
byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(json);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
SessionResponse response = JsonUtility.FromJson<SessionResponse>(request.downloadHandler.text);
onSuccess?.Invoke(response);
// Bei Level-Aenderung cachedLevel updaten
if (response.new_level.HasValue && cachedLevel != null)
{
cachedLevel.overall_level = response.new_level.Value;
}
}
else
{
Debug.LogWarning($"Session speichern fehlgeschlagen: {request.error}");
// Offline: Spaeter synchronisieren
if (useOfflineCache)
{
QueueOfflineSession(session);
}
onError?.Invoke(request.error);
}
}
}
// ==============================================
// Offline-Handling
// ==============================================
private List<GameSession> offlineSessions = new List<GameSession>();
private void QueueOfflineSession(GameSession session)
{
offlineSessions.Add(session);
// Spaeter: PlayerPrefs oder lokale DB nutzen
Debug.Log($"Session zur Offline-Queue hinzugefuegt. Queue-Groesse: {offlineSessions.Count}");
}
public IEnumerator SyncOfflineSessions()
{
var sessionsToSync = new List<GameSession>(offlineSessions);
offlineSessions.Clear();
foreach (var session in sessionsToSync)
{
yield return SaveGameSession(session,
onSuccess: (response) => Debug.Log($"Offline-Session synchronisiert: {response.session_id}"),
onError: (error) => offlineSessions.Add(session) // Zurueck in Queue
);
}
}
// ==============================================
// Helper
// ==============================================
private GameDifficulty GetDefaultDifficulty(int level)
{
// Fallback-Werte wenn API nicht erreichbar
return new GameDifficulty
{
lane_speed = 3f + 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 <= 3,
speech_speed = 0.8f + (level * 0.1f)
};
}
// ==============================================
// Frage fuer visuellen Trigger holen
// ==============================================
public QuizQuestion GetQuestionForTrigger(string triggerType, int difficulty)
{
var matching = cachedQuestions.FindAll(q =>
q.quiz_mode == "quick" &&
q.visual_trigger == triggerType &&
Math.Abs(q.difficulty - difficulty) <= 1
);
if (matching.Count > 0)
{
return matching[UnityEngine.Random.Range(0, matching.Count)];
}
return null;
}
// ==============================================
// Cache-Zugriff
// ==============================================
public LearningLevel GetCachedLevel()
{
return cachedLevel;
}
public List<QuizQuestion> GetCachedQuestions()
{
return cachedQuestions;
}
public GameDifficulty GetCachedDifficulty()
{
return cachedDifficulty;
}
public int GetCachedQuestionsCount()
{
return cachedQuestions.Count;
}
public void ClearQuestionCache()
{
cachedQuestions.Clear();
}
// ==============================================
// Progress API (Phase 5)
// ==============================================
public IEnumerator GetProgress(
string userId,
int days,
Action<ProgressResponse> onSuccess,
Action<string> onError = null)
{
string url = $"{baseUrl}/progress/{userId}?days={days}";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
AddAuthHeader(request);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var response = JsonUtility.FromJson<ProgressResponse>(request.downloadHandler.text);
onSuccess?.Invoke(response);
}
else
{
Debug.LogWarning($"Progress API error: {request.error}");
onError?.Invoke(request.error);
}
}
}
// ==============================================
// Leaderboard API (Phase 5)
// ==============================================
public IEnumerator GetLeaderboard(
string timeframe,
int limit,
bool anonymize,
Action<LeaderboardEntry[]> onSuccess,
Action<string> onError = null)
{
string url = $"{baseUrl}/leaderboard/display?timeframe={timeframe}&limit={limit}&anonymize={anonymize.ToString().ToLower()}";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
string wrappedJson = "{\"entries\":" + request.downloadHandler.text + "}";
var wrapper = JsonUtility.FromJson<LeaderboardWrapper>(wrappedJson);
onSuccess?.Invoke(wrapper.entries);
}
else
{
Debug.LogWarning($"Leaderboard API error: {request.error}");
onError?.Invoke(request.error);
}
}
}
[Serializable]
private class LeaderboardWrapper
{
public LeaderboardEntry[] entries;
}
// ==============================================
// Stats API
// ==============================================
public IEnumerator GetUserStats(
string userId,
Action<UserStats> onSuccess,
Action<string> onError = null)
{
string url = $"{baseUrl}/stats/{userId}";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
AddAuthHeader(request);
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var stats = JsonUtility.FromJson<UserStats>(request.downloadHandler.text);
onSuccess?.Invoke(stats);
}
else
{
Debug.LogWarning($"Stats API error: {request.error}");
onError?.Invoke(request.error);
}
}
}
[Serializable]
public class UserStats
{
public string user_id;
public int overall_level;
public float math_level;
public float german_level;
public float english_level;
public int total_play_time_minutes;
public int total_sessions;
public int questions_answered;
public int questions_correct;
public float accuracy;
}
// ==============================================
// Health Check
// ==============================================
public IEnumerator HealthCheck(Action<bool, string> onComplete)
{
string url = $"{baseUrl}/health";
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
request.timeout = 5;
yield return request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
onComplete?.Invoke(true, request.downloadHandler.text);
}
else
{
onComplete?.Invoke(false, request.error);
}
}
}
}
}

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");
}
}
}

View File

@@ -0,0 +1,373 @@
// ==============================================
// PlayerController.cs - Spieler-Steuerung
// ==============================================
// 3-Spur-System mit Tastatur und Touch-Eingabe.
// Kollisionserkennung fuer Hindernisse und Items.
using System;
using UnityEngine;
namespace BreakpilotDrive
{
public class PlayerController : MonoBehaviour
{
public static PlayerController Instance { get; private set; }
[Header("Bewegung")]
[SerializeField] private float laneDistance = 3f; // Abstand zwischen Spuren
[SerializeField] private float laneSwitchSpeed = 10f; // Geschwindigkeit beim Spurwechsel
[SerializeField] private float forwardSpeed = 10f; // Vorwaertsgeschwindigkeit
[Header("Spuren")]
[SerializeField] private int currentLane = 1; // 0=Links, 1=Mitte, 2=Rechts
private int targetLane = 1;
[Header("Touch-Steuerung")]
[SerializeField] private float swipeThreshold = 50f; // Pixel fuer Swipe-Erkennung
private Vector2 touchStartPos;
private bool isSwiping = false;
[Header("Effekte")]
[SerializeField] private ParticleSystem crashEffect;
[SerializeField] private ParticleSystem collectEffect;
// Zustand
private bool isAlive = true;
private bool isInvincible = false;
private float invincibleTimer = 0f;
// Events
public event Action OnCrash;
public event Action<string> OnItemCollected;
// Components
private Rigidbody rb;
private Animator animator;
void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
rb = GetComponent<Rigidbody>();
animator = GetComponent<Animator>();
}
void Start()
{
// Startposition (mittlere Spur)
currentLane = 1;
targetLane = 1;
UpdateTargetPosition();
// Geschwindigkeit von Difficulty laden
if (GameManager.Instance?.DifficultySettings != null)
{
forwardSpeed = GameManager.Instance.DifficultySettings.lane_speed;
}
}
void Update()
{
if (!isAlive || GameManager.Instance?.CurrentState != GameState.Playing)
return;
// Eingabe verarbeiten
HandleInput();
// Zur Zielspur bewegen
MoveToTargetLane();
// Vorwaerts bewegen
MoveForward();
// Unverwundbarkeit Timer
if (isInvincible)
{
invincibleTimer -= Time.deltaTime;
if (invincibleTimer <= 0)
{
isInvincible = false;
// Blink-Effekt beenden
SetVisibility(true);
}
}
// Distanz zum GameManager melden
GameManager.Instance?.AddDistance(forwardSpeed * Time.deltaTime);
}
// ==============================================
// Eingabe-Verarbeitung
// ==============================================
private void HandleInput()
{
// Tastatur-Eingabe
if (Input.GetKeyDown(KeyCode.A) || Input.GetKeyDown(KeyCode.LeftArrow))
{
MoveLane(-1);
}
else if (Input.GetKeyDown(KeyCode.D) || Input.GetKeyDown(KeyCode.RightArrow))
{
MoveLane(1);
}
// Direkte Spurwahl mit Zahlen (auch fuer Quiz)
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
{
SetLane(0);
}
else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
{
SetLane(1);
}
else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
{
SetLane(2);
}
// Touch-Eingabe
HandleTouchInput();
}
private void HandleTouchInput()
{
if (Input.touchCount > 0)
{
Touch touch = Input.GetTouch(0);
switch (touch.phase)
{
case TouchPhase.Began:
touchStartPos = touch.position;
isSwiping = true;
break;
case TouchPhase.Moved:
case TouchPhase.Ended:
if (isSwiping)
{
Vector2 swipeDelta = touch.position - touchStartPos;
if (Mathf.Abs(swipeDelta.x) > swipeThreshold)
{
if (swipeDelta.x > 0)
{
MoveLane(1); // Rechts
}
else
{
MoveLane(-1); // Links
}
isSwiping = false;
}
}
break;
}
}
// Maus-Simulation (fuer Editor-Tests)
if (Input.GetMouseButtonDown(0))
{
touchStartPos = Input.mousePosition;
isSwiping = true;
}
else if (Input.GetMouseButtonUp(0) && isSwiping)
{
Vector2 swipeDelta = (Vector2)Input.mousePosition - touchStartPos;
if (Mathf.Abs(swipeDelta.x) > swipeThreshold)
{
MoveLane(swipeDelta.x > 0 ? 1 : -1);
}
isSwiping = false;
}
}
// ==============================================
// Spur-Bewegung
// ==============================================
private void MoveLane(int direction)
{
targetLane = Mathf.Clamp(targetLane + direction, 0, 2);
}
private void SetLane(int lane)
{
targetLane = Mathf.Clamp(lane, 0, 2);
}
private void MoveToTargetLane()
{
float targetX = (targetLane - 1) * laneDistance;
Vector3 targetPos = new Vector3(targetX, transform.position.y, transform.position.z);
transform.position = Vector3.Lerp(
transform.position,
targetPos,
laneSwitchSpeed * Time.deltaTime
);
// Aktuelle Spur aktualisieren
currentLane = targetLane;
}
private void MoveForward()
{
// Spieler bleibt stationaer, Welt bewegt sich
// Alternativ: transform.Translate(Vector3.forward * forwardSpeed * Time.deltaTime);
}
private void UpdateTargetPosition()
{
float targetX = (targetLane - 1) * laneDistance;
transform.position = new Vector3(targetX, transform.position.y, transform.position.z);
}
// ==============================================
// Kollisionen
// ==============================================
void OnTriggerEnter(Collider other)
{
if (!isAlive) return;
// Hindernis
if (other.CompareTag("Obstacle"))
{
if (!isInvincible)
{
Crash();
}
}
// Sammelbare Items
else if (other.CompareTag("Coin"))
{
CollectItem("coin", 100);
Destroy(other.gameObject);
}
else if (other.CompareTag("Star"))
{
CollectItem("star", 500);
Destroy(other.gameObject);
}
else if (other.CompareTag("Shield"))
{
CollectItem("shield", 0);
ActivateShield(5f);
Destroy(other.gameObject);
}
// Quiz-Trigger
else if (other.CompareTag("QuizTrigger"))
{
VisualTrigger trigger = other.GetComponent<VisualTrigger>();
if (trigger != null)
{
QuizManager.Instance?.ShowQuickQuestion(trigger.TriggerType);
}
}
}
// ==============================================
// Crash-Handling
// ==============================================
private void Crash()
{
// Effekt abspielen
if (crashEffect != null)
{
crashEffect.Play();
}
// Animation
if (animator != null)
{
animator.SetTrigger("Crash");
}
// Leben verlieren
GameManager.Instance?.LoseLife();
// Kurze Unverwundbarkeit
isInvincible = true;
invincibleTimer = 2f;
StartCoroutine(BlinkEffect());
OnCrash?.Invoke();
}
private System.Collections.IEnumerator BlinkEffect()
{
while (isInvincible)
{
SetVisibility(false);
yield return new WaitForSeconds(0.1f);
SetVisibility(true);
yield return new WaitForSeconds(0.1f);
}
}
private void SetVisibility(bool visible)
{
Renderer[] renderers = GetComponentsInChildren<Renderer>();
foreach (var r in renderers)
{
r.enabled = visible;
}
}
// ==============================================
// Item-Collection
// ==============================================
private void CollectItem(string itemType, int points)
{
// Effekt abspielen
if (collectEffect != null)
{
collectEffect.Play();
}
// Punkte hinzufuegen
if (points > 0)
{
GameManager.Instance?.AddScore(points);
}
OnItemCollected?.Invoke(itemType);
}
private void ActivateShield(float duration)
{
isInvincible = true;
invincibleTimer = duration;
// TODO: Schild-Visualisierung
}
// ==============================================
// Oeffentliche Methoden
// ==============================================
public void SetSpeed(float speed)
{
forwardSpeed = speed;
}
public int GetCurrentLane()
{
return currentLane;
}
public void Reset()
{
isAlive = true;
isInvincible = false;
currentLane = 1;
targetLane = 1;
UpdateTargetPosition();
}
}
}

View File

@@ -0,0 +1,54 @@
// ==============================================
// AuthPlugin.jslib - WebGL JavaScript Bridge
// ==============================================
// Place in Assets/Plugins/WebGL/
mergeInto(LibraryManager.library, {
// Get token from URL parameter (?token=xxx)
GetTokenFromURL: function() {
var urlParams = new URLSearchParams(window.location.search);
var token = urlParams.get('token');
if (token) {
var bufferSize = lengthBytesUTF8(token) + 1;
var buffer = _malloc(bufferSize);
stringToUTF8(token, buffer, bufferSize);
return buffer;
}
return null;
},
// Get token from parent frame (for iframe embedding)
GetTokenFromParent: function() {
if (window.breakpilotToken) {
var token = window.breakpilotToken;
var bufferSize = lengthBytesUTF8(token) + 1;
var buffer = _malloc(bufferSize);
stringToUTF8(token, buffer, bufferSize);
return buffer;
}
return null;
},
// Request token from parent frame via postMessage
RequestTokenFromParent: function() {
// Listen for token from parent
window.addEventListener('message', function(event) {
// Verify origin in production!
if (event.data && event.data.type === 'breakpilot_token') {
window.breakpilotToken = event.data.token;
// Send to Unity
if (window.unityInstance) {
window.unityInstance.SendMessage('AuthManager', 'ReceiveTokenFromJS', event.data.token);
}
}
}, false);
// Request token from parent
if (window.parent !== window) {
window.parent.postMessage({ type: 'breakpilot_request_token' }, '*');
}
}
});

View File

@@ -0,0 +1,98 @@
// ==============================================
// WebSpeech.jslib - Text-to-Speech fuer WebGL
// ==============================================
// Kopiere diese Datei nach Assets/Plugins/WebGL/
// um TTS im Browser zu aktivieren.
mergeInto(LibraryManager.library, {
// Spricht den Text sofort
SpeakWebGL: function(textPtr) {
var text = UTF8ToString(textPtr);
if ('speechSynthesis' in window) {
// Vorherige Sprache stoppen
window.speechSynthesis.cancel();
var utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'de-DE';
utterance.rate = 0.9;
utterance.pitch = 1.0;
utterance.volume = 1.0;
// Deutsche Stimme suchen
var voices = window.speechSynthesis.getVoices();
var germanVoice = voices.find(function(voice) {
return voice.lang.startsWith('de');
});
if (germanVoice) {
utterance.voice = germanVoice;
}
window.speechSynthesis.speak(utterance);
} else {
console.warn('Text-to-Speech wird von diesem Browser nicht unterstuetzt.');
}
},
// Spricht den Text nach einer Verzoegerung
SpeakDelayedWebGL: function(textPtr, delaySeconds) {
var text = UTF8ToString(textPtr);
setTimeout(function() {
if ('speechSynthesis' in window) {
var utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'de-DE';
utterance.rate = 0.9;
utterance.pitch = 1.0;
utterance.volume = 1.0;
var voices = window.speechSynthesis.getVoices();
var germanVoice = voices.find(function(voice) {
return voice.lang.startsWith('de');
});
if (germanVoice) {
utterance.voice = germanVoice;
}
window.speechSynthesis.speak(utterance);
}
}, delaySeconds * 1000);
},
// Stoppt alle laufenden Sprach-Ausgaben
StopSpeakingWebGL: function() {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
}
},
// Prueft ob TTS verfuegbar ist
IsTTSAvailableWebGL: function() {
return 'speechSynthesis' in window;
},
// Gibt verfuegbare Stimmen zurueck (als JSON)
GetAvailableVoicesWebGL: function() {
if ('speechSynthesis' in window) {
var voices = window.speechSynthesis.getVoices();
var germanVoices = voices.filter(function(voice) {
return voice.lang.startsWith('de');
}).map(function(voice) {
return {
name: voice.name,
lang: voice.lang,
local: voice.localService
};
});
var json = JSON.stringify(germanVoices);
var bufferSize = lengthBytesUTF8(json) + 1;
var buffer = _malloc(bufferSize);
stringToUTF8(json, buffer, bufferSize);
return buffer;
}
return null;
}
});

View File

@@ -0,0 +1,318 @@
// ==============================================
// QuizManager.cs - Quiz-Steuerung
// ==============================================
// Verwaltet Quick-Fragen (waehrend Fahrt) und
// Pause-Fragen (Spiel haelt an).
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace BreakpilotDrive
{
public class QuizManager : MonoBehaviour
{
public static QuizManager Instance { get; private set; }
[Header("UI Referenzen")]
[SerializeField] private GameObject quickQuizPanel;
[SerializeField] private GameObject pauseQuizPanel;
[SerializeField] private TextMeshProUGUI questionText;
[SerializeField] private Button[] answerButtons;
[SerializeField] private TextMeshProUGUI timerText;
[SerializeField] private Slider timerSlider;
[Header("Audio (fuer Audio-Version)")]
[SerializeField] private AudioSource audioSource;
[SerializeField] private bool isAudioMode = false;
[Header("Einstellungen")]
[SerializeField] private int pointsCorrect = 500;
[SerializeField] private int pointsWrong = -100;
[SerializeField] private float pauseQuestionInterval = 500f; // Alle X Meter
// Aktueller Zustand
private QuizQuestion currentQuestion;
private bool isQuizActive = false;
private float timeRemaining;
private Coroutine timerCoroutine;
// Events
public event Action<bool, int> OnQuestionAnswered; // (correct, points)
public event Action OnQuizStarted;
public event Action OnQuizEnded;
// Statistik
private int questionsAnswered = 0;
private int questionsCorrect = 0;
void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
void Start()
{
// UI initial verstecken
if (quickQuizPanel) quickQuizPanel.SetActive(false);
if (pauseQuizPanel) pauseQuizPanel.SetActive(false);
// Answer Buttons einrichten
for (int i = 0; i < answerButtons.Length; i++)
{
int index = i; // Capture fuer Lambda
answerButtons[i].onClick.AddListener(() => OnAnswerSelected(index));
}
}
void Update()
{
// Keyboard-Eingabe fuer Antworten (1-4)
if (isQuizActive && currentQuestion != null)
{
if (Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1))
OnAnswerSelected(0);
else if (Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2))
OnAnswerSelected(1);
else if (Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3))
OnAnswerSelected(2);
else if (Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4))
OnAnswerSelected(3);
}
}
// ==============================================
// Quick Quiz (waehrend der Fahrt)
// ==============================================
public void ShowQuickQuestion(string visualTrigger)
{
if (isQuizActive) return;
// Frage aus Cache holen
int difficulty = BreakpilotAPI.Instance != null ?
(int)GetCurrentDifficulty() : 3;
QuizQuestion question = BreakpilotAPI.Instance?.GetQuestionForTrigger(visualTrigger, difficulty);
if (question == null)
{
Debug.LogWarning($"Keine Frage fuer Trigger '{visualTrigger}' gefunden");
return;
}
currentQuestion = question;
isQuizActive = true;
// UI anzeigen
if (quickQuizPanel) quickQuizPanel.SetActive(true);
DisplayQuestion(question);
// Timer starten
float timeLimit = question.time_limit_seconds > 0 ? question.time_limit_seconds : 5f;
timerCoroutine = StartCoroutine(QuickQuizTimer(timeLimit));
// Audio-Version: Frage vorlesen
if (isAudioMode)
{
SpeakQuestion(question);
}
OnQuizStarted?.Invoke();
}
private IEnumerator QuickQuizTimer(float duration)
{
timeRemaining = duration;
while (timeRemaining > 0)
{
timeRemaining -= Time.deltaTime;
// UI updaten
if (timerText) timerText.text = $"{timeRemaining:F1}s";
if (timerSlider) timerSlider.value = timeRemaining / duration;
yield return null;
}
// Zeit abgelaufen = falsche Antwort
OnAnswerSelected(-1);
}
// ==============================================
// Pause Quiz (Spiel haelt an)
// ==============================================
public void ShowPauseQuestion(QuizQuestion question)
{
if (isQuizActive) return;
currentQuestion = question;
isQuizActive = true;
// Spiel pausieren
Time.timeScale = 0;
// UI anzeigen
if (pauseQuizPanel) pauseQuizPanel.SetActive(true);
DisplayQuestion(question);
// Audio-Version: Frage vorlesen
if (isAudioMode)
{
SpeakQuestion(question);
}
OnQuizStarted?.Invoke();
}
// ==============================================
// Antwort verarbeiten
// ==============================================
private void OnAnswerSelected(int index)
{
if (!isQuizActive || currentQuestion == null) return;
// Timer stoppen
if (timerCoroutine != null)
{
StopCoroutine(timerCoroutine);
timerCoroutine = null;
}
bool isCorrect = index == currentQuestion.correct_index;
int points = isCorrect ? pointsCorrect : pointsWrong;
questionsAnswered++;
if (isCorrect) questionsCorrect++;
// Visuelles Feedback
ShowAnswerFeedback(isCorrect, currentQuestion.correct_index);
// Audio Feedback
if (isAudioMode)
{
SpeakFeedback(isCorrect);
}
// Event ausloesen
OnQuestionAnswered?.Invoke(isCorrect, points);
// Quiz beenden (mit kurzer Verzoegerung fuer Feedback)
StartCoroutine(EndQuizAfterDelay(isCorrect ? 0.5f : 1f));
}
private IEnumerator EndQuizAfterDelay(float delay)
{
// Bei pausiertem Spiel: unscaled Zeit nutzen
yield return new WaitForSecondsRealtime(delay);
EndQuiz();
}
private void EndQuiz()
{
isQuizActive = false;
currentQuestion = null;
// UI verstecken
if (quickQuizPanel) quickQuizPanel.SetActive(false);
if (pauseQuizPanel) pauseQuizPanel.SetActive(false);
// Spiel fortsetzen (falls pausiert)
Time.timeScale = 1;
OnQuizEnded?.Invoke();
}
// ==============================================
// UI Helper
// ==============================================
private void DisplayQuestion(QuizQuestion question)
{
if (questionText) questionText.text = question.question_text;
for (int i = 0; i < answerButtons.Length; i++)
{
if (i < question.options.Length)
{
answerButtons[i].gameObject.SetActive(true);
var buttonText = answerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
if (buttonText) buttonText.text = $"[{i + 1}] {question.options[i]}";
}
else
{
answerButtons[i].gameObject.SetActive(false);
}
}
}
private void ShowAnswerFeedback(bool correct, int correctIndex)
{
// Richtige Antwort gruen markieren
if (correctIndex >= 0 && correctIndex < answerButtons.Length)
{
var colors = answerButtons[correctIndex].colors;
colors.normalColor = Color.green;
answerButtons[correctIndex].colors = colors;
}
// TODO: Animation, Sound, Partikel
}
// ==============================================
// Audio-Version
// ==============================================
private void SpeakQuestion(QuizQuestion question)
{
// TODO: Text-to-Speech Integration
// Option 1: Voraufgenommene Audio-Dateien
// Option 2: Web-TTS API
// Option 3: Unity Speech Synthesis (platform-abhaengig)
Debug.Log($"[TTS] {question.question_text}");
for (int i = 0; i < question.options.Length; i++)
{
Debug.Log($"[TTS] {i + 1} fuer {question.options[i]}");
}
}
private void SpeakFeedback(bool correct)
{
string message = correct ? "Richtig! Gut gemacht!" : "Nicht ganz. Versuch es nochmal!";
Debug.Log($"[TTS] {message}");
}
// ==============================================
// Statistik
// ==============================================
public int GetQuestionsAnswered() => questionsAnswered;
public int GetQuestionsCorrect() => questionsCorrect;
public float GetAccuracy()
{
if (questionsAnswered == 0) return 0;
return (float)questionsCorrect / questionsAnswered;
}
public void ResetStatistics()
{
questionsAnswered = 0;
questionsCorrect = 0;
}
private float GetCurrentDifficulty()
{
// TODO: Aus GameManager holen
return 3;
}
}
}

View File

@@ -0,0 +1,248 @@
// ==============================================
// ObstacleSpawner.cs - Hindernis-Generator
// ==============================================
// Spawnt Hindernisse, Items und Quiz-Trigger
// basierend auf Schwierigkeitsgrad.
using UnityEngine;
namespace BreakpilotDrive
{
public class ObstacleSpawner : MonoBehaviour
{
public static ObstacleSpawner Instance { get; private set; }
[Header("Hindernis-Prefabs")]
[SerializeField] private GameObject[] obstaclePrefabs; // Verschiedene Hindernisse
[SerializeField] private GameObject[] largObstaclePrefabs; // Grosse Hindernisse (2 Spuren)
[Header("Item-Prefabs")]
[SerializeField] private GameObject coinPrefab;
[SerializeField] private GameObject starPrefab;
[SerializeField] private GameObject shieldPrefab;
[Header("Quiz-Trigger-Prefabs")]
[SerializeField] private GameObject bridgePrefab;
[SerializeField] private GameObject treePrefab;
[SerializeField] private GameObject housePrefab;
[Header("Spawn-Einstellungen")]
[SerializeField] private float laneDistance = 3f;
[SerializeField] private float minObstacleSpacing = 10f;
[SerializeField] private float maxObstacleSpacing = 30f;
[Header("Wahrscheinlichkeiten (0-1)")]
[SerializeField] private float obstacleChance = 0.6f;
[SerializeField] private float coinChance = 0.3f;
[SerializeField] private float starChance = 0.05f;
[SerializeField] private float shieldChance = 0.02f;
[SerializeField] private float quizTriggerChance = 0.1f;
// Interner Zustand
private float lastObstacleZ = 0f;
private int quizTriggerCount = 0;
void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
void Start()
{
// Wahrscheinlichkeiten von Difficulty laden
UpdateFromDifficulty();
}
private void UpdateFromDifficulty()
{
if (GameManager.Instance?.DifficultySettings != null)
{
var settings = GameManager.Instance.DifficultySettings;
obstacleChance = settings.obstacle_frequency;
// Power-Up Chance beeinflusst Items
float powerUpChance = settings.power_up_chance;
coinChance = powerUpChance * 0.8f;
starChance = powerUpChance * 0.15f;
shieldChance = powerUpChance * 0.05f;
}
}
// ==============================================
// Spawning auf Segment
// ==============================================
public void SpawnOnSegment(GameObject segment, float segmentStartZ)
{
float segmentLength = 50f; // Sollte mit TrackGenerator uebereinstimmen
float currentZ = segmentStartZ + minObstacleSpacing;
while (currentZ < segmentStartZ + segmentLength - minObstacleSpacing)
{
// Zufaelliger Abstand zum naechsten Objekt
float spacing = Random.Range(minObstacleSpacing, maxObstacleSpacing);
// Was spawnen?
SpawnAtPosition(segment.transform, currentZ);
currentZ += spacing;
}
}
private void SpawnAtPosition(Transform parent, float zPosition)
{
float roll = Random.value;
// Quiz-Trigger (selten, aber wichtig)
if (roll < quizTriggerChance && quizTriggerCount < 3)
{
SpawnQuizTrigger(parent, zPosition);
quizTriggerCount++;
return;
}
// Hindernis
roll = Random.value;
if (roll < obstacleChance)
{
SpawnObstacle(parent, zPosition);
}
// Items (koennen neben Hindernissen sein)
roll = Random.value;
if (roll < coinChance)
{
SpawnCoinRow(parent, zPosition);
}
else if (roll < coinChance + starChance)
{
SpawnItem(starPrefab, parent, zPosition, GetRandomLane());
}
else if (roll < coinChance + starChance + shieldChance)
{
SpawnItem(shieldPrefab, parent, zPosition, GetRandomLane());
}
}
// ==============================================
// Hindernisse
// ==============================================
private void SpawnObstacle(Transform parent, float zPosition)
{
if (obstaclePrefabs == null || obstaclePrefabs.Length == 0) return;
// Zufaelliges Hindernis und Spur waehlen
int prefabIndex = Random.Range(0, obstaclePrefabs.Length);
int lane = GetRandomLane();
// Manchmal grosses Hindernis (blockiert 2 Spuren)
if (largObstaclePrefabs != null && largObstaclePrefabs.Length > 0 && Random.value < 0.2f)
{
prefabIndex = Random.Range(0, largObstaclePrefabs.Length);
SpawnObstacleAtLane(largObstaclePrefabs[prefabIndex], parent, zPosition, lane);
}
else
{
SpawnObstacleAtLane(obstaclePrefabs[prefabIndex], parent, zPosition, lane);
}
}
private void SpawnObstacleAtLane(GameObject prefab, Transform parent, float zPosition, int lane)
{
float xPos = (lane - 1) * laneDistance;
Vector3 position = new Vector3(xPos, 0, zPosition);
GameObject obstacle = Instantiate(prefab, parent);
obstacle.transform.localPosition = position;
obstacle.tag = "Obstacle";
}
// ==============================================
// Items
// ==============================================
private void SpawnItem(GameObject prefab, Transform parent, float zPosition, int lane)
{
if (prefab == null) return;
float xPos = (lane - 1) * laneDistance;
Vector3 position = new Vector3(xPos, 0.5f, zPosition); // Leicht ueber dem Boden
GameObject item = Instantiate(prefab, parent);
item.transform.localPosition = position;
}
private void SpawnCoinRow(Transform parent, float zPosition)
{
if (coinPrefab == null) return;
int lane = GetRandomLane();
int coinCount = Random.Range(3, 6);
for (int i = 0; i < coinCount; i++)
{
SpawnItem(coinPrefab, parent, zPosition + (i * 2f), lane);
}
}
// ==============================================
// Quiz-Trigger
// ==============================================
private void SpawnQuizTrigger(Transform parent, float zPosition)
{
GameObject prefab = GetRandomTriggerPrefab();
if (prefab == null) return;
// Trigger sind meist in der Mitte oder ueber der Strasse
Vector3 position = new Vector3(0, 0, zPosition);
GameObject trigger = Instantiate(prefab, parent);
trigger.transform.localPosition = position;
trigger.tag = "QuizTrigger";
// VisualTrigger-Komponente hinzufuegen falls nicht vorhanden
if (trigger.GetComponent<VisualTrigger>() == null)
{
VisualTrigger vt = trigger.AddComponent<VisualTrigger>();
vt.TriggerType = GetTriggerTypeFromPrefab(prefab);
}
}
private GameObject GetRandomTriggerPrefab()
{
GameObject[] triggers = new GameObject[] { bridgePrefab, treePrefab, housePrefab };
var validTriggers = System.Array.FindAll(triggers, t => t != null);
if (validTriggers.Length == 0) return null;
return validTriggers[Random.Range(0, validTriggers.Length)];
}
private string GetTriggerTypeFromPrefab(GameObject prefab)
{
if (prefab == bridgePrefab) return "bridge";
if (prefab == treePrefab) return "tree";
if (prefab == housePrefab) return "house";
return "unknown";
}
// ==============================================
// Helper
// ==============================================
private int GetRandomLane()
{
return Random.Range(0, 3); // 0, 1, oder 2
}
public void ResetSpawner()
{
lastObstacleZ = 0f;
quizTriggerCount = 0;
UpdateFromDifficulty();
}
}
}

View File

@@ -0,0 +1,233 @@
// ==============================================
// TrackGenerator.cs - Endlose Streckengenerierung
// ==============================================
// Erzeugt endlose Streckenabschnitte durch
// Object-Pooling und Recycling.
using System.Collections.Generic;
using UnityEngine;
namespace BreakpilotDrive
{
public class TrackGenerator : MonoBehaviour
{
public static TrackGenerator Instance { get; private set; }
[Header("Track Prefabs")]
[SerializeField] private GameObject[] trackSegmentPrefabs; // Verschiedene Streckenabschnitte
[SerializeField] private GameObject startSegmentPrefab; // Start-Segment
[Header("Generierung")]
[SerializeField] private float segmentLength = 50f; // Laenge eines Segments
[SerializeField] private int visibleSegments = 5; // Anzahl sichtbarer Segmente
[SerializeField] private float despawnDistance = -20f; // Wann Segment recycelt wird
[Header("Bewegung")]
[SerializeField] private float baseSpeed = 10f; // Basis-Geschwindigkeit
private float currentSpeed;
// Object Pool
private List<GameObject> activeSegments = new List<GameObject>();
private Queue<GameObject> segmentPool = new Queue<GameObject>();
// Position
private float nextSpawnZ = 0f;
void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
}
}
void Start()
{
currentSpeed = baseSpeed;
// Geschwindigkeit von Difficulty laden
if (GameManager.Instance?.DifficultySettings != null)
{
currentSpeed = GameManager.Instance.DifficultySettings.lane_speed;
}
// Initiale Segmente erstellen
InitializeTrack();
}
void Update()
{
if (GameManager.Instance?.CurrentState != GameState.Playing)
return;
// Segmente bewegen
MoveSegments();
// Alte Segmente recyceln
RecycleSegments();
// Neue Segmente spawnen
SpawnSegmentsIfNeeded();
}
// ==============================================
// Initialisierung
// ==============================================
private void InitializeTrack()
{
nextSpawnZ = 0f;
// Start-Segment
if (startSegmentPrefab != null)
{
SpawnSegment(startSegmentPrefab);
}
// Weitere Segmente fuer sichtbaren Bereich
for (int i = 0; i < visibleSegments; i++)
{
SpawnRandomSegment();
}
}
// ==============================================
// Segment-Spawning
// ==============================================
private void SpawnRandomSegment()
{
if (trackSegmentPrefabs == null || trackSegmentPrefabs.Length == 0)
{
Debug.LogWarning("Keine Track-Segment-Prefabs zugewiesen!");
return;
}
int index = Random.Range(0, trackSegmentPrefabs.Length);
SpawnSegment(trackSegmentPrefabs[index]);
}
private void SpawnSegment(GameObject prefab)
{
GameObject segment;
// Aus Pool holen oder neu erstellen
if (segmentPool.Count > 0)
{
segment = segmentPool.Dequeue();
segment.SetActive(true);
}
else
{
segment = Instantiate(prefab, transform);
}
// Position setzen
segment.transform.position = new Vector3(0, 0, nextSpawnZ);
nextSpawnZ += segmentLength;
activeSegments.Add(segment);
// Hindernisse und Items auf Segment spawnen
ObstacleSpawner.Instance?.SpawnOnSegment(segment, nextSpawnZ - segmentLength);
}
private void SpawnSegmentsIfNeeded()
{
// Pruefe ob wir mehr Segmente brauchen
float playerZ = 0; // Spieler ist bei Z=0, Welt bewegt sich
float maxVisibleZ = playerZ + (visibleSegments * segmentLength);
while (nextSpawnZ < maxVisibleZ)
{
SpawnRandomSegment();
}
}
// ==============================================
// Bewegung
// ==============================================
private void MoveSegments()
{
Vector3 movement = Vector3.back * currentSpeed * Time.deltaTime;
foreach (var segment in activeSegments)
{
segment.transform.position += movement;
}
// Auch nextSpawnZ anpassen
nextSpawnZ -= currentSpeed * Time.deltaTime;
}
// ==============================================
// Recycling
// ==============================================
private void RecycleSegments()
{
for (int i = activeSegments.Count - 1; i >= 0; i--)
{
if (activeSegments[i].transform.position.z < despawnDistance)
{
RecycleSegment(activeSegments[i]);
activeSegments.RemoveAt(i);
}
}
}
private void RecycleSegment(GameObject segment)
{
// Kinder-Objekte (Hindernisse, Items) entfernen
foreach (Transform child in segment.transform)
{
if (child.CompareTag("Obstacle") || child.CompareTag("Coin") ||
child.CompareTag("Star") || child.CompareTag("QuizTrigger"))
{
Destroy(child.gameObject);
}
}
// Segment deaktivieren und zurueck in Pool
segment.SetActive(false);
segmentPool.Enqueue(segment);
}
// ==============================================
// Geschwindigkeit
// ==============================================
public void SetSpeed(float speed)
{
currentSpeed = speed;
}
public float GetSpeed()
{
return currentSpeed;
}
public void IncreaseSpeed(float amount)
{
currentSpeed += amount;
}
// ==============================================
// Reset
// ==============================================
public void ResetTrack()
{
// Alle aktiven Segmente recyceln
foreach (var segment in activeSegments)
{
RecycleSegment(segment);
}
activeSegments.Clear();
// Neu initialisieren
nextSpawnZ = 0f;
currentSpeed = baseSpeed;
InitializeTrack();
}
}
}

View File

@@ -0,0 +1,57 @@
// ==============================================
// VisualTrigger.cs - Quiz-Trigger Objekt
// ==============================================
// Loest Quick-Quiz aus wenn Spieler durchfaehrt.
using UnityEngine;
namespace BreakpilotDrive
{
public class VisualTrigger : MonoBehaviour
{
[Header("Trigger-Konfiguration")]
[SerializeField] private string triggerType = "bridge"; // "bridge", "tree", "house"
public string TriggerType
{
get => triggerType;
set => triggerType = value;
}
private bool hasTriggered = false;
void OnTriggerEnter(Collider other)
{
if (hasTriggered) return;
if (other.CompareTag("Player"))
{
hasTriggered = true;
// Quick-Quiz ausloesen
if (QuizManager.Instance != null)
{
QuizManager.Instance.ShowQuickQuestion(triggerType);
}
Debug.Log($"Quiz-Trigger ausgeloest: {triggerType}");
}
}
void OnTriggerExit(Collider other)
{
// Reset fuer naechsten Durchlauf (falls Objekt recycelt wird)
if (other.CompareTag("Player"))
{
// Optional: Trigger nach kurzer Zeit wieder aktivieren
// hasTriggered = false;
}
}
// Wird aufgerufen wenn Objekt recycelt wird
void OnDisable()
{
hasTriggered = false;
}
}
}

View File

@@ -0,0 +1,144 @@
// ==============================================
// GameHUD.cs - Spiel-UI Anzeige
// ==============================================
// Zeigt Score, Leben, Distanz und andere
// Spielinformationen an.
using UnityEngine;
using TMPro;
using UnityEngine.UI;
namespace BreakpilotDrive
{
public class GameHUD : MonoBehaviour
{
[Header("Score")]
[SerializeField] private TextMeshProUGUI scoreText;
[SerializeField] private TextMeshProUGUI highScoreText;
[Header("Leben")]
[SerializeField] private TextMeshProUGUI livesText;
[SerializeField] private Image[] heartImages;
[Header("Distanz")]
[SerializeField] private TextMeshProUGUI distanceText;
[Header("Quiz-Statistik")]
[SerializeField] private TextMeshProUGUI quizStatsText;
[Header("Animationen")]
[SerializeField] private Animator scoreAnimator;
private int displayedScore = 0;
private int targetScore = 0;
void Start()
{
// Events abonnieren
if (GameManager.Instance != null)
{
GameManager.Instance.OnScoreChanged += UpdateScore;
GameManager.Instance.OnLivesChanged += UpdateLives;
GameManager.Instance.OnDistanceChanged += UpdateDistance;
}
// Initial-Werte setzen
UpdateScore(0);
UpdateLives(3);
UpdateDistance(0);
}
void OnDestroy()
{
// Events abbestellen
if (GameManager.Instance != null)
{
GameManager.Instance.OnScoreChanged -= UpdateScore;
GameManager.Instance.OnLivesChanged -= UpdateLives;
GameManager.Instance.OnDistanceChanged -= UpdateDistance;
}
}
void Update()
{
// Score-Animation (zaehlt hoch)
if (displayedScore != targetScore)
{
displayedScore = (int)Mathf.MoveTowards(displayedScore, targetScore, Time.deltaTime * 1000);
if (scoreText != null)
{
scoreText.text = displayedScore.ToString("N0");
}
}
// Quiz-Statistik aktualisieren
UpdateQuizStats();
}
// ==============================================
// Score
// ==============================================
private void UpdateScore(int newScore)
{
targetScore = newScore;
// Animation ausloesen bei Punkte-Gewinn
if (newScore > displayedScore && scoreAnimator != null)
{
scoreAnimator.SetTrigger("ScoreUp");
}
}
// ==============================================
// Leben
// ==============================================
private void UpdateLives(int lives)
{
if (livesText != null)
{
livesText.text = $"x{lives}";
}
// Herz-Icons aktualisieren
if (heartImages != null)
{
for (int i = 0; i < heartImages.Length; i++)
{
heartImages[i].enabled = i < lives;
}
}
}
// ==============================================
// Distanz
// ==============================================
private void UpdateDistance(float distance)
{
if (distanceText != null)
{
distanceText.text = $"{distance:F0}m";
}
}
// ==============================================
// Quiz-Statistik
// ==============================================
private void UpdateQuizStats()
{
if (quizStatsText == null || QuizManager.Instance == null) return;
int answered = QuizManager.Instance.GetQuestionsAnswered();
int correct = QuizManager.Instance.GetQuestionsCorrect();
if (answered > 0)
{
float accuracy = QuizManager.Instance.GetAccuracy() * 100;
quizStatsText.text = $"Quiz: {correct}/{answered} ({accuracy:F0}%)";
}
else
{
quizStatsText.text = "Quiz: 0/0";
}
}
}
}

View File

@@ -0,0 +1,180 @@
// ==============================================
// MainMenu.cs - Hauptmenue Steuerung
// ==============================================
// Verwaltet das Startmenue mit Spielmodus-Auswahl
// und Einstellungen.
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;
namespace BreakpilotDrive
{
public class MainMenu : MonoBehaviour
{
[Header("UI Elemente")]
[SerializeField] private GameObject mainPanel;
[SerializeField] private GameObject settingsPanel;
[SerializeField] private GameObject loadingPanel;
[Header("Benutzer-Info")]
[SerializeField] private TMP_InputField userIdInput;
[SerializeField] private TextMeshProUGUI levelText;
[SerializeField] private TextMeshProUGUI welcomeText;
[Header("Scene-Namen")]
[SerializeField] private string videoGameScene = "Game_Video";
[SerializeField] private string audioGameScene = "Game_Audio";
// Benutzer-Daten
private string currentUserId = "guest";
private LearningLevel currentLevel;
void Start()
{
// Panels initialisieren
if (mainPanel) mainPanel.SetActive(true);
if (settingsPanel) settingsPanel.SetActive(false);
if (loadingPanel) loadingPanel.SetActive(false);
// Gespeicherte User-ID laden
currentUserId = PlayerPrefs.GetString("UserId", "guest");
if (userIdInput != null)
{
userIdInput.text = currentUserId;
}
// Lernniveau laden
LoadUserLevel();
}
// ==============================================
// Benutzer-Management
// ==============================================
public void OnUserIdChanged(string newUserId)
{
currentUserId = string.IsNullOrEmpty(newUserId) ? "guest" : newUserId;
PlayerPrefs.SetString("UserId", currentUserId);
PlayerPrefs.Save();
LoadUserLevel();
}
private void LoadUserLevel()
{
if (BreakpilotAPI.Instance != null)
{
StartCoroutine(BreakpilotAPI.Instance.GetLearningLevel(currentUserId,
onSuccess: (level) =>
{
currentLevel = level;
UpdateLevelDisplay();
},
onError: (error) =>
{
Debug.LogWarning($"Lernniveau konnte nicht geladen werden: {error}");
// Fallback-Level
currentLevel = new LearningLevel { overall_level = 3 };
UpdateLevelDisplay();
}
));
}
}
private void UpdateLevelDisplay()
{
if (levelText != null && currentLevel != null)
{
levelText.text = $"Level {currentLevel.overall_level}";
}
if (welcomeText != null)
{
string name = currentUserId == "guest" ? "Gast" : currentUserId;
welcomeText.text = $"Hallo, {name}!";
}
}
// ==============================================
// Spielstart
// ==============================================
public void PlayVideoMode()
{
StartGame(videoGameScene);
}
public void PlayAudioMode()
{
StartGame(audioGameScene);
}
private void StartGame(string sceneName)
{
// Loading anzeigen
if (loadingPanel) loadingPanel.SetActive(true);
if (mainPanel) mainPanel.SetActive(false);
// Fragen vorladen
if (BreakpilotAPI.Instance != null)
{
int difficulty = currentLevel?.overall_level ?? 3;
StartCoroutine(BreakpilotAPI.Instance.GetQuizQuestions(
difficulty: difficulty,
count: 20,
onSuccess: (questions) =>
{
Debug.Log($"{questions.Length} Fragen vorgeladen");
LoadScene(sceneName);
},
onError: (error) =>
{
Debug.LogWarning($"Fragen konnten nicht geladen werden: {error}");
// Trotzdem starten (Offline-Modus)
LoadScene(sceneName);
}
));
}
else
{
LoadScene(sceneName);
}
}
private void LoadScene(string sceneName)
{
SceneManager.LoadScene(sceneName);
}
// ==============================================
// Einstellungen
// ==============================================
public void OpenSettings()
{
if (mainPanel) mainPanel.SetActive(false);
if (settingsPanel) settingsPanel.SetActive(true);
}
public void CloseSettings()
{
if (settingsPanel) settingsPanel.SetActive(false);
if (mainPanel) mainPanel.SetActive(true);
}
// ==============================================
// Sonstiges
// ==============================================
public void QuitGame()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
public void OpenWebsite()
{
Application.OpenURL("https://breakpilot.app");
}
}
}

View File

@@ -0,0 +1,340 @@
// ==============================================
// QuizOverlay.cs - Quiz-UI Anzeige
// ==============================================
// Zeigt Quiz-Fragen an (Quick und Pause Mode)
// mit Timer, Antwort-Buttons und Feedback.
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
namespace BreakpilotDrive
{
public class QuizOverlay : MonoBehaviour
{
[Header("Panels")]
[SerializeField] private GameObject quickQuizPanel;
[SerializeField] private GameObject pauseQuizPanel;
[SerializeField] private GameObject feedbackPanel;
[Header("Quick Quiz UI")]
[SerializeField] private TextMeshProUGUI quickQuestionText;
[SerializeField] private Button[] quickAnswerButtons;
[SerializeField] private Slider timerSlider;
[SerializeField] private TextMeshProUGUI timerText;
[Header("Pause Quiz UI")]
[SerializeField] private TextMeshProUGUI pauseQuestionText;
[SerializeField] private Button[] pauseAnswerButtons;
[SerializeField] private TextMeshProUGUI pauseHintText;
[Header("Feedback UI")]
[SerializeField] private TextMeshProUGUI feedbackText;
[SerializeField] private Image feedbackIcon;
[SerializeField] private Sprite correctIcon;
[SerializeField] private Sprite wrongIcon;
[Header("Farben")]
[SerializeField] private Color normalColor = Color.white;
[SerializeField] private Color correctColor = Color.green;
[SerializeField] private Color wrongColor = Color.red;
[SerializeField] private Color selectedColor = Color.yellow;
[Header("Animation")]
[SerializeField] private Animator quizAnimator;
[SerializeField] private float feedbackDuration = 1.5f;
// Aktuelle Frage
private QuizQuestion currentQuestion;
private int selectedAnswer = -1;
private bool isAnswered = false;
void Start()
{
// Panels verstecken
HideAll();
// QuizManager Events abonnieren
if (QuizManager.Instance != null)
{
QuizManager.Instance.OnQuizStarted += OnQuizStarted;
QuizManager.Instance.OnQuizEnded += OnQuizEnded;
QuizManager.Instance.OnQuestionAnswered += OnQuestionAnswered;
}
// Button-Listener einrichten
SetupButtons();
}
void OnDestroy()
{
if (QuizManager.Instance != null)
{
QuizManager.Instance.OnQuizStarted -= OnQuizStarted;
QuizManager.Instance.OnQuizEnded -= OnQuizEnded;
QuizManager.Instance.OnQuestionAnswered -= OnQuestionAnswered;
}
}
// ==============================================
// Button Setup
// ==============================================
private void SetupButtons()
{
// Quick Answer Buttons
for (int i = 0; i < quickAnswerButtons.Length; i++)
{
int index = i;
quickAnswerButtons[i].onClick.AddListener(() => OnAnswerClicked(index));
}
// Pause Answer Buttons
for (int i = 0; i < pauseAnswerButtons.Length; i++)
{
int index = i;
pauseAnswerButtons[i].onClick.AddListener(() => OnAnswerClicked(index));
}
}
// ==============================================
// Quiz Events
// ==============================================
private void OnQuizStarted()
{
isAnswered = false;
selectedAnswer = -1;
}
private void OnQuizEnded()
{
StartCoroutine(HideAfterDelay(feedbackDuration));
}
private void OnQuestionAnswered(bool correct, int points)
{
isAnswered = true;
ShowFeedback(correct, points);
}
// ==============================================
// Quick Quiz anzeigen
// ==============================================
public void ShowQuickQuiz(QuizQuestion question, float timeLimit)
{
currentQuestion = question;
isAnswered = false;
// Panel aktivieren
HideAll();
if (quickQuizPanel) quickQuizPanel.SetActive(true);
// Frage anzeigen
if (quickQuestionText) quickQuestionText.text = question.question_text;
// Antworten anzeigen
for (int i = 0; i < quickAnswerButtons.Length; i++)
{
if (i < question.options.Length)
{
quickAnswerButtons[i].gameObject.SetActive(true);
var text = quickAnswerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
if (text) text.text = $"[{i + 1}] {question.options[i]}";
ResetButtonColor(quickAnswerButtons[i]);
}
else
{
quickAnswerButtons[i].gameObject.SetActive(false);
}
}
// Timer starten
StartCoroutine(UpdateTimer(timeLimit));
// Animation
if (quizAnimator) quizAnimator.SetTrigger("ShowQuick");
}
private IEnumerator UpdateTimer(float duration)
{
float timeRemaining = duration;
while (timeRemaining > 0 && !isAnswered)
{
timeRemaining -= Time.deltaTime;
// UI aktualisieren
if (timerSlider) timerSlider.value = timeRemaining / duration;
if (timerText) timerText.text = $"{timeRemaining:F1}s";
// Farbe aendern wenn Zeit knapp
if (timerSlider && timeRemaining < duration * 0.3f)
{
timerSlider.fillRect.GetComponent<Image>().color = Color.red;
}
yield return null;
}
}
// ==============================================
// Pause Quiz anzeigen
// ==============================================
public void ShowPauseQuiz(QuizQuestion question)
{
currentQuestion = question;
isAnswered = false;
// Panel aktivieren
HideAll();
if (pauseQuizPanel) pauseQuizPanel.SetActive(true);
// Frage anzeigen
if (pauseQuestionText) pauseQuestionText.text = question.question_text;
// Hint anzeigen (falls aktiviert)
if (pauseHintText)
{
bool hintsEnabled = GameManager.Instance?.DifficultySettings?.hints_enabled ?? false;
pauseHintText.gameObject.SetActive(hintsEnabled);
// TODO: Hint-Text generieren
}
// Antworten anzeigen
for (int i = 0; i < pauseAnswerButtons.Length; i++)
{
if (i < question.options.Length)
{
pauseAnswerButtons[i].gameObject.SetActive(true);
var text = pauseAnswerButtons[i].GetComponentInChildren<TextMeshProUGUI>();
if (text) text.text = $"[{i + 1}] {question.options[i]}";
ResetButtonColor(pauseAnswerButtons[i]);
}
else
{
pauseAnswerButtons[i].gameObject.SetActive(false);
}
}
// Animation
if (quizAnimator) quizAnimator.SetTrigger("ShowPause");
}
// ==============================================
// Antwort-Handling
// ==============================================
private void OnAnswerClicked(int index)
{
if (isAnswered || currentQuestion == null) return;
selectedAnswer = index;
// Button hervorheben
Button[] buttons = currentQuestion.quiz_mode == "quick" ?
quickAnswerButtons : pauseAnswerButtons;
if (index < buttons.Length)
{
HighlightButton(buttons[index]);
}
// An QuizManager melden (wird dort verarbeitet)
// QuizManager ruft dann OnQuestionAnswered auf
}
// ==============================================
// Feedback anzeigen
// ==============================================
private void ShowFeedback(bool correct, int points)
{
// Richtige Antwort markieren
Button[] buttons = currentQuestion?.quiz_mode == "quick" ?
quickAnswerButtons : pauseAnswerButtons;
if (currentQuestion != null && buttons != null)
{
int correctIndex = currentQuestion.correct_index;
if (correctIndex >= 0 && correctIndex < buttons.Length)
{
SetButtonColor(buttons[correctIndex], correctColor);
}
// Falsche Antwort rot markieren
if (!correct && selectedAnswer >= 0 && selectedAnswer < buttons.Length)
{
SetButtonColor(buttons[selectedAnswer], wrongColor);
}
}
// Feedback Panel
if (feedbackPanel)
{
feedbackPanel.SetActive(true);
if (feedbackText)
{
string pointsStr = points >= 0 ? $"+{points}" : $"{points}";
feedbackText.text = correct ?
$"Richtig! {pointsStr} Punkte" :
$"Leider falsch. {pointsStr} Punkte";
feedbackText.color = correct ? correctColor : wrongColor;
}
if (feedbackIcon)
{
feedbackIcon.sprite = correct ? correctIcon : wrongIcon;
}
}
// Sound abspielen
if (correct)
{
AudioManager.Instance?.PlayCorrectSound();
}
else
{
AudioManager.Instance?.PlayWrongSound();
}
// Animation
if (quizAnimator)
{
quizAnimator.SetTrigger(correct ? "Correct" : "Wrong");
}
}
// ==============================================
// Helper
// ==============================================
private void HideAll()
{
if (quickQuizPanel) quickQuizPanel.SetActive(false);
if (pauseQuizPanel) pauseQuizPanel.SetActive(false);
if (feedbackPanel) feedbackPanel.SetActive(false);
}
private IEnumerator HideAfterDelay(float delay)
{
yield return new WaitForSecondsRealtime(delay);
HideAll();
}
private void ResetButtonColor(Button button)
{
SetButtonColor(button, normalColor);
}
private void HighlightButton(Button button)
{
SetButtonColor(button, selectedColor);
}
private void SetButtonColor(Button button, Color color)
{
var colors = button.colors;
colors.normalColor = color;
colors.highlightedColor = color;
button.colors = colors;
}
}
}

View File

@@ -0,0 +1,298 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>{{{ PRODUCT_NAME }}}</title>
<!-- PWA Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="theme-color" content="#1a1a2e">
<!-- Favicon -->
<link rel="shortcut icon" href="TemplateData/favicon.ico">
<link rel="apple-touch-icon" href="TemplateData/icon-192.png">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #1a1a2e;
}
#unity-container {
position: absolute;
width: 100%;
height: 100%;
}
#unity-canvas {
width: 100%;
height: 100%;
background: #1a1a2e;
}
#unity-loading-bar {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 400px;
}
#unity-logo {
text-align: center;
margin-bottom: 20px;
}
#unity-logo img {
max-width: 200px;
}
#unity-title {
color: #fff;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 24px;
text-align: center;
margin-bottom: 20px;
}
#unity-progress-bar-empty {
width: 100%;
height: 18px;
background: #333;
border-radius: 9px;
overflow: hidden;
}
#unity-progress-bar-full {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #4CAF50, #8BC34A);
transition: width 0.3s ease;
}
#unity-loading-text {
color: #aaa;
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 14px;
text-align: center;
margin-top: 10px;
}
#unity-warning {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: #fff;
padding: 20px;
border-radius: 10px;
text-align: center;
display: none;
}
#unity-footer {
position: absolute;
bottom: 10px;
width: 100%;
text-align: center;
}
#unity-fullscreen-button {
background: transparent;
border: none;
cursor: pointer;
padding: 10px;
}
#unity-fullscreen-button img {
width: 38px;
height: 38px;
opacity: 0.7;
transition: opacity 0.3s;
}
#unity-fullscreen-button:hover img {
opacity: 1;
}
.hidden {
display: none !important;
}
/* Mobile Optimierungen */
@media (max-width: 768px) {
#unity-title {
font-size: 18px;
}
#unity-loading-bar {
width: 90%;
}
}
</style>
</head>
<body>
<div id="unity-container">
<!-- Loading Screen -->
<div id="unity-loading-bar">
<div id="unity-logo">
<img src="TemplateData/logo.png" alt="Breakpilot Drive">
</div>
<div id="unity-title">{{{ PRODUCT_NAME }}}</div>
<div id="unity-progress-bar-empty">
<div id="unity-progress-bar-full"></div>
</div>
<div id="unity-loading-text">Lade...</div>
</div>
<!-- Game Canvas -->
<canvas id="unity-canvas" tabindex="-1"></canvas>
<!-- Warning Message -->
<div id="unity-warning"></div>
<!-- Footer -->
<div id="unity-footer">
<button id="unity-fullscreen-button" title="Vollbild">
<img src="TemplateData/fullscreen-button.png" alt="Vollbild">
</button>
</div>
</div>
<script>
var container = document.querySelector("#unity-container");
var canvas = document.querySelector("#unity-canvas");
var loadingBar = document.querySelector("#unity-loading-bar");
var progressBarFull = document.querySelector("#unity-progress-bar-full");
var loadingText = document.querySelector("#unity-loading-text");
var fullscreenButton = document.querySelector("#unity-fullscreen-button");
var warningBanner = document.querySelector("#unity-warning");
// Zeigt Warnungen/Fehler an
function unityShowBanner(msg, type) {
function updateBannerVisibility() {
warningBanner.style.display = warningBanner.children.length ? 'block' : 'none';
}
var div = document.createElement('div');
div.innerHTML = msg;
warningBanner.appendChild(div);
if (type == 'error') div.style = 'color: #ff0000;';
else if (type == 'warning') div.style = 'color: #ff9900;';
setTimeout(function() {
warningBanner.removeChild(div);
updateBannerVisibility();
}, 5000);
updateBannerVisibility();
}
// Build-Konfiguration
var buildUrl = "Build";
var loaderUrl = buildUrl + "/{{{ LOADER_FILENAME }}}";
var config = {
dataUrl: buildUrl + "/{{{ DATA_FILENAME }}}",
frameworkUrl: buildUrl + "/{{{ FRAMEWORK_FILENAME }}}",
#if USE_WASM
codeUrl: buildUrl + "/{{{ CODE_FILENAME }}}",
#endif
#if MEMORY_FILENAME
memoryUrl: buildUrl + "/{{{ MEMORY_FILENAME }}}",
#endif
#if SYMBOLS_FILENAME
symbolsUrl: buildUrl + "/{{{ SYMBOLS_FILENAME }}}",
#endif
streamingAssetsUrl: "StreamingAssets",
companyName: "{{{ COMPANY_NAME }}}",
productName: "{{{ PRODUCT_NAME }}}",
productVersion: "{{{ PRODUCT_VERSION }}}",
showBanner: unityShowBanner,
};
// Mobile Check
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
var meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, height=device-height, initial-scale=1.0, user-scalable=no, shrink-to-fit=yes';
document.getElementsByTagName('head')[0].appendChild(meta);
container.className = "unity-mobile";
}
// Canvas-Groesse anpassen
canvas.style.width = "100%";
canvas.style.height = "100%";
// Loader-Script laden
var script = document.createElement("script");
script.src = loaderUrl;
script.onload = () => {
createUnityInstance(canvas, config, (progress) => {
// Progress-Bar aktualisieren
var percent = Math.round(progress * 100);
progressBarFull.style.width = percent + "%";
loadingText.textContent = "Lade... " + percent + "%";
}).then((unityInstance) => {
// Loading fertig
loadingBar.classList.add("hidden");
// Fullscreen-Button
fullscreenButton.onclick = () => {
unityInstance.SetFullscreen(1);
};
// Unity-Instanz global verfuegbar machen
window.unityInstance = unityInstance;
}).catch((message) => {
alert(message);
});
};
document.body.appendChild(script);
// Text-to-Speech Funktionen (fuer Unity)
window.SpeakWebGL = function(text) {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
var utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'de-DE';
utterance.rate = 0.9;
var voices = window.speechSynthesis.getVoices();
var germanVoice = voices.find(v => v.lang.startsWith('de'));
if (germanVoice) utterance.voice = germanVoice;
window.speechSynthesis.speak(utterance);
}
};
window.SpeakDelayedWebGL = function(text, delay) {
setTimeout(function() {
window.SpeakWebGL(text);
}, delay * 1000);
};
window.StopSpeakingWebGL = function() {
if ('speechSynthesis' in window) {
window.speechSynthesis.cancel();
}
};
// Voice-Liste laden (manche Browser laden sie async)
if ('speechSynthesis' in window) {
window.speechSynthesis.onvoiceschanged = function() {
console.log('Voices geladen:', window.speechSynthesis.getVoices().length);
};
}
</script>
</body>
</html>