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>
319 lines
10 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|