// GateSpawner.cs // Zweck: Spawnt Gates zur aktuellen Frage (Tutorial/JSON), setzt Frage-UI, wertet Ergebnisse aus und geht weiter. using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; public class GateSpawner : MonoBehaviour { // === Prefabs & Referenzen === public GameObject gatePrefab; public Transform player; // Schiffstransform public Transform roadCenter; // Bahnmittelpunkt (Fallback: player) public TMP_Text questionTextDisplay; public RawImage questionImageDisplay; public AudioSource questionAudioSource; [Header("Ship Feedback")] [SerializeField] private ShipFeedbackController shipFeedback; [Header("Ship")] [SerializeField] private ShipMovement shipMovement; // === SFX === public AudioSource sfxSource; public string correctClipName = "correctSparkles"; public string wrongClipName = "wrongShock"; public float sfxVolume = 1f; private AudioClip _correctClip; private AudioClip _wrongClip; // === Gate Layout === public int maxGatesPerQuestion = 5; public float spacing = 120f; public float spawnAheadDistance = 1000f; public float roadHalfWidth = 80f; // Boden/Höhe public float fallbackGroundY = 0f; public LayerMask groundLayer; // Sicherheits-Trigger wenn alle Gates übersprungen werden public float skipTriggerOffset = 25f; public Vector2 skipTriggerSize = new Vector2(80f, 20f); public float skipTriggerThickness = 5f; [Header("Road bounds")] [SerializeField] private float edgePadding = 2f; // Randabstand private GameObject activeSkipTrigger; private readonly List activeGates = new(); // === Tutorial (fixe Fragen) === [Header("Tutorial mode (fixed questions)")] [SerializeField] private Question[] tutorialQuestions; private int _tutorialIndex = 0; private bool _tutorialActive = false; private int _tutorialAnswered = 0; [SerializeField] private int tutorialTargetAnswers = 2; // Events für TutorialFlightController public System.Action OnTutorialQuestionAnswered; public System.Action OnTutorialSequenceFinished; private void Awake() { // Fragebild initial aus if (questionImageDisplay != null) questionImageDisplay.gameObject.SetActive(false); // SFX laden _correctClip = Resources.Load(correctClipName); _wrongClip = Resources.Load(wrongClipName); if (!_correctClip) Debug.LogWarning($"Correct SFX nicht gefunden: {correctClipName}"); if (!_wrongClip) Debug.LogWarning($"Wrong SFX nicht gefunden: {wrongClipName}"); } // === JSON-Spielstart === public void BeginGame() { _tutorialActive = false; _tutorialIndex = 0; _tutorialAnswered = 0; ScoreHUD.Instance?.ResetScore(); QuestionManager.Instance?.ResetToFirstQuestion(); EnsureShipMovement(); if (shipMovement) shipMovement.ResetAdaptiveSpeed(); SpawnCurrentQuestionGates(); } // Alles leeren (Gates/Frageanzeige/Audio) public void ClearAll() { ClearExistingGates(); if (questionTextDisplay) questionTextDisplay.text = ""; if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false); if (questionAudioSource) questionAudioSource.Stop(); } // Lazy-Referenzen beschaffen private void EnsureShipFeedback() { if (shipFeedback != null || player == null) return; shipFeedback = player.GetComponentInChildren(); if (!shipFeedback) Debug.LogWarning("[GateSpawner] ShipFeedbackController nicht gefunden."); } private void EnsureShipMovement() { if (shipMovement != null || player == null) return; shipMovement = player.GetComponentInChildren(); if (!shipMovement) Debug.LogWarning("[GateSpawner] ShipMovement nicht gefunden."); } // === Tutorial-Ablauf === public void BeginTutorialQuestions() { _tutorialActive = true; _tutorialIndex = 0; _tutorialAnswered = 0; ClearAll(); ScoreHUD.Instance?.ResetScore(); if (tutorialQuestions != null && tutorialQuestions.Length > 0) SpawnGatesForQuestion(tutorialQuestions[_tutorialIndex]); else { Debug.LogWarning("[GateSpawner] Keine Tutorialfragen zugewiesen."); _tutorialActive = false; OnTutorialSequenceFinished?.Invoke(); } } public void StopTutorialQuestions() { _tutorialActive = false; _tutorialIndex = 0; _tutorialAnswered = 0; ClearAll(); } private void SpawnNextTutorialQuestion() { _tutorialIndex++; if (tutorialQuestions == null || _tutorialIndex >= tutorialQuestions.Length) { ClearAll(); _tutorialActive = false; OnTutorialSequenceFinished?.Invoke(); } else { SpawnGatesForQuestion(tutorialQuestions[_tutorialIndex]); } } // === JSON-Flow: aktuelle Frage spawnen === public void SpawnCurrentQuestionGates() { ClearExistingGates(); if (!player) { Debug.LogWarning("[GateSpawner] Player-Transform fehlt."); return; } var q = QuestionManager.Instance != null ? QuestionManager.Instance.GetCurrentQuestion() : null; if (q == null) { if (questionTextDisplay) questionTextDisplay.text = ""; if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false); return; } SpawnGatesForQuestion(q); } // === Gemeinsamer Spawner (Tutorial + JSON) === private void SpawnGatesForQuestion(Question q) { if (q == null) { Debug.LogWarning("[GateSpawner] Frage ist null."); return; } if (!player) { Debug.LogWarning("[GateSpawner] Player-Transform fehlt."); return; } // Antworten filtern (brauchen Text ODER Bild) var raw = q.answers != null ? new List(q.answers) : new List(); var usable = new List(); foreach (var a in raw) { if (a == null) continue; bool hasText = !string.IsNullOrWhiteSpace(a.text); bool hasImage = !string.IsNullOrWhiteSpace(a.image); if (hasText || hasImage) usable.Add(a); } if (usable.Count == 0) { Debug.LogWarning("Frage hat keine verwendbaren Antworten."); if (questionTextDisplay) questionTextDisplay.text = ""; if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false); return; } int gatesToSpawn = Mathf.Min(maxGatesPerQuestion, usable.Count); // Shuffle Kopie für die Platzierung var shuffled = new List(usable); ShuffleList(shuffled); // Korrekte Antwort bestimmen (Index clampen) int clampedCorrect = Mathf.Clamp(q.correctAnswerIndex, 0, usable.Count - 1); Answer correctAnswer = usable[clampedCorrect]; // Frage-UI (Text) if (questionTextDisplay != null) { if (!string.IsNullOrEmpty(q.questionText)) { questionTextDisplay.gameObject.SetActive(true); questionTextDisplay.text = q.questionText; } else questionTextDisplay.gameObject.SetActive(false); } // Frage-UI (Bild) if (questionImageDisplay != null) { if (!string.IsNullOrEmpty(q.questionImage)) { string imageName = System.IO.Path.GetFileNameWithoutExtension(q.questionImage); Texture2D qTex = Resources.Load(imageName); if (qTex != null) { questionImageDisplay.texture = qTex; questionImageDisplay.gameObject.SetActive(true); } else { Debug.LogWarning("Fragebild nicht gefunden: " + imageName); questionImageDisplay.gameObject.SetActive(false); } } else questionImageDisplay.gameObject.SetActive(false); } // Frage-VO (optional) if (questionAudioSource != null) { if (!string.IsNullOrEmpty(q.questionSound)) { string soundName = System.IO.Path.GetFileNameWithoutExtension(q.questionSound); AudioClip clip = Resources.Load(soundName); if (clip != null) { questionAudioSource.clip = clip; questionAudioSource.PlayDelayed(0.5f); } else Debug.LogWarning("Fragesound nicht gefunden: " + soundName); } else questionAudioSource.clip = null; } // Platzierungsbasis (Forward/Right) Transform basis = roadCenter ? roadCenter : player; Vector3 fwd = Vector3.ProjectOnPlane(basis.forward, Vector3.up).normalized; if (fwd.sqrMagnitude < 1e-4f) fwd = Vector3.forward; Vector3 right = Vector3.Cross(Vector3.up, fwd).normalized; float playerAlong = Vector3.Dot(player.position - basis.position, fwd); Vector3 startLineCenter = basis.position + fwd * (playerAlong + spawnAheadDistance); // Nutzbare Breite innerhalb der Straße float leftEdge = -roadHalfWidth + edgePadding; float rightEdge = roadHalfWidth - edgePadding; for (int i = 0; i < gatesToSpawn; i++) { Vector3 gatePos; // Modusabhängige Platzierung if (GameModeManager.Instance != null && GameModeManager.Instance.SelectedMode == GameMode.Linear) { if (gatesToSpawn == 1) gatePos = startLineCenter; else { float t = (float)i / (gatesToSpawn - 1); float xOffset = Mathf.Lerp(leftEdge, rightEdge, t); gatePos = startLineCenter + right * xOffset; } } else { float xOffset = Random.Range(leftEdge, rightEdge); gatePos = startLineCenter + fwd * (i * spacing) + right * xOffset; } gatePos = SnapToGround(gatePos, fallbackGroundY); Quaternion gateRot = Quaternion.LookRotation(fwd, Vector3.up); GameObject gate = Instantiate(gatePrefab, gatePos, gateRot); // Antwort-Inhalte befüllen Answer a = shuffled[i]; TMP_Text textComp = gate.GetComponentInChildren(true); if (textComp != null) { if (!string.IsNullOrEmpty(a.text)) { textComp.gameObject.SetActive(true); textComp.text = a.text; } else textComp.gameObject.SetActive(false); } var gateQuestion = gate.GetComponentInChildren(); if (gateQuestion != null) gateQuestion.SetQuestionText(a.text); RawImage rawImg = gate.GetComponentInChildren(true); Image uiImg = rawImg ? null : gate.GetComponentInChildren(true); if (rawImg != null || uiImg != null) { if (!string.IsNullOrEmpty(a.image)) { string imageName = System.IO.Path.GetFileNameWithoutExtension(a.image); Texture2D tex = Resources.Load(imageName); if (tex != null) { if (rawImg) { rawImg.texture = tex; rawImg.gameObject.SetActive(true); } else { uiImg.sprite = SpriteFromTexture(tex); uiImg.gameObject.SetActive(true); } } else { Debug.LogWarning("Antwortbild nicht gefunden: " + imageName); if (rawImg) rawImg.gameObject.SetActive(false); if (uiImg) uiImg.gameObject.SetActive(false); } } else { if (rawImg) rawImg.gameObject.SetActive(false); if (uiImg) uiImg.gameObject.SetActive(false); } } var gateAnswer = gate.GetComponentInChildren(); if (gateAnswer != null) { gateAnswer.answerIndex = usable.IndexOf(a); gateAnswer.isCorrect = (a == correctAnswer); gateAnswer.spawner = this; } activeGates.Add(gate); } if (activeGates.Count > 0) PlaceSkipTrigger(basis, fwd, right); } // === Callback von GateAnswer === public void HandleGateAnswered(bool wasCorrect) { // 1) SFX/Feedback var clip = wasCorrect ? _correctClip : _wrongClip; if (clip != null) { if (sfxSource) sfxSource.PlayOneShot(clip, sfxVolume); else AudioSource.PlayClipAtPoint(clip, player ? player.position : transform.position, sfxVolume); } // 2) Adaptive Geschwindigkeit EnsureShipMovement(); if (shipMovement) shipMovement.ApplyAnswerResult(wasCorrect); // 3) Score if (wasCorrect) ScoreHUD.Instance?.AddPoint(1); // 4) Alte Gates weg ClearExistingGates(); // 5) Tutorial? if (_tutorialActive) { OnTutorialQuestionAnswered?.Invoke(wasCorrect); _tutorialAnswered++; if (_tutorialAnswered >= tutorialTargetAnswers) { _tutorialActive = false; if (questionTextDisplay) questionTextDisplay.text = ""; if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false); OnTutorialSequenceFinished?.Invoke(); return; } SpawnNextTutorialQuestion(); return; } // 6) JSON-Flow: nächste Frage var qm = QuestionManager.Instance; if (qm == null) { Debug.LogWarning("[GateSpawner] QuestionManager.Instance ist null."); return; } // Nächste Frage anwählen qm.AdvanceToNextQuestion(); Question nextQuestion = qm.GetCurrentQuestion(); if (nextQuestion == null) { // === HIER: Alle Fragen sind beantwortet === // Frage-UI leeren if (questionTextDisplay) questionTextDisplay.text = ""; if (questionImageDisplay) questionImageDisplay.gameObject.SetActive(false); if (questionAudioSource) questionAudioSource.Stop(); // Schiff anhalten, damit man nicht ins Nichts rauscht EnsureShipMovement(); if (shipMovement) shipMovement.SetActive(false); // Endscreen anzeigen (Text + Button-Hinweis) if (EndingCanvas.Instance != null) { EndingCanvas.Instance.Show( "Glückwunsch! Du hast alle Fragen geschafft.\n\n" + "Drücke den angezeigten Button, um zum Cockpit-Menü zurückzukehren." ); } else { Debug.LogWarning("[GateSpawner] EndOfQuestionsScreen.Instance fehlt – kein Endbildschirm angezeigt."); } return; } // Es gibt noch Fragen -> ganz normal nächste Runde spawnen SpawnGatesForQuestion(nextQuestion); } // === Hilfen === private void PlaceSkipTrigger(Transform basis, Vector3 fwd, Vector3 right) { // Finde weitestes Gate (entlang fwd) float maxAlong = float.NegativeInfinity; Vector3 farthestPos = Vector3.zero; foreach (var gate in activeGates) { if (!gate) continue; Vector3 toGate = gate.transform.position - basis.position; float along = Vector3.Dot(toGate, fwd); if (along > maxAlong) { maxAlong = along; farthestPos = gate.transform.position; } } Vector3 triggerPos = farthestPos + fwd * skipTriggerOffset; triggerPos = SnapToGround(triggerPos, fallbackGroundY); if (activeSkipTrigger != null) { Destroy(activeSkipTrigger); activeSkipTrigger = null; } activeSkipTrigger = new GameObject("Safety Trigger"); activeSkipTrigger.layer = gameObject.layer; activeSkipTrigger.transform.position = triggerPos; activeSkipTrigger.transform.rotation = Quaternion.LookRotation(fwd, Vector3.up); var box = activeSkipTrigger.AddComponent(); box.isTrigger = true; box.size = new Vector3(skipTriggerSize.x, skipTriggerSize.y, skipTriggerThickness); var rb = activeSkipTrigger.AddComponent(); rb.isKinematic = true; rb.useGravity = false; var logic = activeSkipTrigger.AddComponent(); logic.spawner = this; } private Vector3 SnapToGround(Vector3 pos, float fallbackY) { if (groundLayer.value != 0) { Vector3 origin = pos + Vector3.up * 200f; if (Physics.Raycast(origin, Vector3.down, out var hit, 500f, groundLayer, QueryTriggerInteraction.Ignore)) { pos.y = hit.point.y; return pos; } } pos.y = fallbackY; return pos; } private void ClearExistingGates() { foreach (var g in activeGates) if (g) Destroy(g); activeGates.Clear(); if (activeSkipTrigger != null) { Destroy(activeSkipTrigger); activeSkipTrigger = null; } } private void ShuffleList(List list) { for (int i = 0; i < list.Count; i++) { int r = Random.Range(i, list.Count); (list[i], list[r]) = (list[r], list[i]); } } private static Sprite SpriteFromTexture(Texture2D tex) { if (!tex) return null; return Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100f); } public IReadOnlyList ActiveGates => activeGates; }