2025-12-11 13:14:43 +01:00

542 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<GameObject> 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<bool> OnTutorialQuestionAnswered;
public System.Action OnTutorialSequenceFinished;
private void Awake()
{
// Fragebild initial aus
if (questionImageDisplay != null)
questionImageDisplay.gameObject.SetActive(false);
// SFX laden
_correctClip = Resources.Load<AudioClip>(correctClipName);
_wrongClip = Resources.Load<AudioClip>(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<ShipFeedbackController>();
if (!shipFeedback) Debug.LogWarning("[GateSpawner] ShipFeedbackController nicht gefunden.");
}
private void EnsureShipMovement()
{
if (shipMovement != null || player == null) return;
shipMovement = player.GetComponentInChildren<ShipMovement>();
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<Answer>(q.answers) : new List<Answer>();
var usable = new List<Answer>();
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<Answer>(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<Texture2D>(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<AudioClip>(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<TMP_Text>(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<GateText>();
if (gateQuestion != null) gateQuestion.SetQuestionText(a.text);
RawImage rawImg = gate.GetComponentInChildren<RawImage>(true);
Image uiImg = rawImg ? null : gate.GetComponentInChildren<Image>(true);
if (rawImg != null || uiImg != null)
{
if (!string.IsNullOrEmpty(a.image))
{
string imageName = System.IO.Path.GetFileNameWithoutExtension(a.image);
Texture2D tex = Resources.Load<Texture2D>(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<GateAnswer>();
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<BoxCollider>();
box.isTrigger = true;
box.size = new Vector3(skipTriggerSize.x, skipTriggerSize.y, skipTriggerThickness);
var rb = activeSkipTrigger.AddComponent<Rigidbody>();
rb.isKinematic = true; rb.useGravity = false;
var logic = activeSkipTrigger.AddComponent<SafetyTrigger>();
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<T>(List<T> 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<GameObject> ActiveGates => activeGates;
}