This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Benjamin Admin 21a844cb8a 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>
2026-02-09 09:51:32 +01:00

319 lines
10 KiB
C#

// ==============================================
// 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;
}
}
}