542 lines
18 KiB
C#
542 lines
18 KiB
C#
// 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;
|
||
}
|