// TutorialFlightController.cs // Zweck: Schritt-für-Schritt Flug-Tutorial (Hebel -> Vorwärts -> Kopfneigung -> Fragen -> Abschluss). using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.InputSystem; public class TutorialFlightController : MonoBehaviour { public enum TutorialPhase { Idle, Welcome, ExplainLever, WaitForForwardFlight, ExplainHeadTilt, WaitHeadPlay, ExplainQuestions, InQuestions, FinalCongrats } [Header("References")] [SerializeField] private ShipMovement shipMovement; [SerializeField] private ThrottleLever throttle; [SerializeField] private GateSpawner gateSpawner; [SerializeField] private CanvasGroup textCanvas; [SerializeField] private TMP_Text textLabel; [SerializeField] private InputActionReference advanceAction; [Header("Lever Highlight (Pulse)")] [SerializeField] private Color pulseColor = Color.cyan; [SerializeField] private float pulseSpeed = 2.2f; [SerializeField] private float pulseMin = 0.15f; [SerializeField] private float pulseMax = 1.0f; [Header("Flow settings")] [SerializeField] private float requiredTravelDistance = 100f; [SerializeField] private float headPlayDuration = 5f; [Header("Tutorial Q&A")] [SerializeField] private int tutorialAnswersTarget = 2; // Zustand private TutorialPhase phase = TutorialPhase.Idle; private CockpitTransitionController transitionController; private Vector3 travelStartPos; private float headPlayTimer; // Hebel-Visuals private Renderer[] leverRenderers; private MaterialPropertyBlock[] mpbs; private bool leverPulseActive; private Coroutine pulseCo; private static readonly int _ColorID = Shader.PropertyToID("_Color"); private static readonly int _EmissionColorID = Shader.PropertyToID("_EmissionColor"); // Pager private List _currentPages = new List(); private int _pageIndex = -1; // Tutorial Q Flow private int tutorialAnswers = 0; private bool tutorialActive = false; private void Awake() { if (textCanvas) { textCanvas.alpha = 0f; textCanvas.interactable = false; textCanvas.blocksRaycasts = false; if (!textCanvas.gameObject.activeSelf) textCanvas.gameObject.SetActive(true); } } private void OnEnable() { if (advanceAction != null) { advanceAction.action.performed += OnAdvance; advanceAction.action.Enable(); } } private void OnDisable() { if (advanceAction != null) { advanceAction.action.performed -= OnAdvance; advanceAction.action.Disable(); } if (gateSpawner != null) { gateSpawner.OnTutorialQuestionAnswered -= OnTutorialQuestionAnswered; gateSpawner.OnTutorialSequenceFinished -= OnTutorialSequenceFinished; } StopLeverPulse(); } // Einstieg: Referenzen setzen, Events binden, Hebel-Renderer sammeln public void BeginTutorial(CockpitTransitionController transition, ShipMovement ship, GateSpawner spawner) { transitionController = transition; shipMovement = ship; gateSpawner = spawner; if (gateSpawner != null) { gateSpawner.OnTutorialQuestionAnswered -= OnTutorialQuestionAnswered; gateSpawner.OnTutorialSequenceFinished -= OnTutorialSequenceFinished; gateSpawner.OnTutorialQuestionAnswered += OnTutorialQuestionAnswered; gateSpawner.OnTutorialSequenceFinished += OnTutorialSequenceFinished; } if (throttle && throttle.handle) { leverRenderers = throttle.handle.GetComponentsInChildren(true); if (leverRenderers != null && leverRenderers.Length > 0) { mpbs = new MaterialPropertyBlock[leverRenderers.Length]; for (int i = 0; i < leverRenderers.Length; i++) mpbs[i] = new MaterialPropertyBlock(); } } if (shipMovement) { shipMovement.SetActive(false); shipMovement.HeadTiltEnabled = false; } if (throttle) throttle.SetGrabEnabled(true); tutorialAnswers = 0; tutorialActive = false; ShowPages( "Willkommen im Tutorial! Hier lernst du Schritt für Schritt, wie du das Schiff steuerst.", "Um das Schiff zu starten, greife nach dem Hebel rechts vor dir.", "Sobald dein rechter Controller den Hebel berührt, halte den rechten Trigger gedrückt und schiebe den Hebel nach vorn, um zu beschleunigen.", "Um wieder langsamer zu werden, ziehe den Hebel nach hinten. Probier es aus!" ); phase = TutorialPhase.Welcome; } private void Update() { switch (phase) { case TutorialPhase.WaitForForwardFlight: UpdateWaitForForwardFlight(); break; case TutorialPhase.WaitHeadPlay: UpdateWaitHeadPlay(); break; } } // Pager private void ShowPages(params string[] pages) { _currentPages.Clear(); if (pages != null) _currentPages.AddRange(pages); _pageIndex = -1; AdvancePage(); } private bool AdvancePage() { if (_currentPages.Count == 0) return false; _pageIndex++; if (_pageIndex >= _currentPages.Count) return false; if (textLabel) textLabel.text = _currentPages[_pageIndex]; if (textCanvas) { textCanvas.alpha = 1f; textCanvas.interactable = true; textCanvas.blocksRaycasts = true; if (!textCanvas.gameObject.activeSelf) textCanvas.gameObject.SetActive(true); } return true; } private void ClearPages() { _currentPages.Clear(); _pageIndex = -1; } private void HideText() { if (textCanvas) { textCanvas.alpha = 0f; textCanvas.interactable = false; textCanvas.blocksRaycasts = false; } } private void OnAdvance(InputAction.CallbackContext _) { if (textCanvas == null || textCanvas.alpha <= 0.01f) return; if (AdvancePage()) return; switch (phase) { case TutorialPhase.Welcome: case TutorialPhase.ExplainLever: HideText(); ClearPages(); HighlightLeverAndEnableFlight(); break; case TutorialPhase.ExplainHeadTilt: HideText(); ClearPages(); if (shipMovement) { shipMovement.HeadTiltEnabled = true; shipMovement.SetActive(true); } headPlayTimer = 0f; phase = TutorialPhase.WaitHeadPlay; break; case TutorialPhase.ExplainQuestions: HideText(); ClearPages(); if (gateSpawner) gateSpawner.BeginTutorialQuestions(); if (shipMovement) { shipMovement.SetActive(true); shipMovement.HeadTiltEnabled = true; } tutorialActive = true; phase = TutorialPhase.InQuestions; break; case TutorialPhase.FinalCongrats: HideText(); ClearPages(); if (gateSpawner) gateSpawner.StopTutorialQuestions(); if (shipMovement) { shipMovement.SetActive(false); shipMovement.HeadTiltEnabled = true; shipMovement.ResetToStartPose(); } if (throttle) throttle.ResetToZero(); ScoreHUD.Instance?.ResetScore(); transitionController?.EndTutorialReturnToMenu(); phase = TutorialPhase.Idle; break; } } // Schritte private void HighlightLeverAndEnableFlight() { if (shipMovement) { shipMovement.HeadTiltEnabled = false; // Tilt off shipMovement.SetActive(true); // nur Vorwärts travelStartPos = shipMovement.MoveRoot.position; } if (throttle) throttle.SetGrabEnabled(true); StartLeverPulse(); phase = TutorialPhase.WaitForForwardFlight; } private void UpdateWaitForForwardFlight() { if (!shipMovement) return; float dist = Vector3.Distance(shipMovement.MoveRoot.position, travelStartPos); if (dist >= requiredTravelDistance) CompleteForwardFlightStep(); } private void CompleteForwardFlightStep() { if (shipMovement) shipMovement.SetActive(false); StopLeverPulse(); ShowPages( "Super!", "Jetzt versuche das Schiff nach links und rechts zu steuern", "Dafür neigst du deinen Kopf einfach zur linken oder rechten Seite." ); phase = TutorialPhase.ExplainHeadTilt; } private void UpdateWaitHeadPlay() { headPlayTimer += Time.deltaTime; if (headPlayTimer >= headPlayDuration) CompleteHeadPlayStep(); } private void CompleteHeadPlayStep() { if (shipMovement) shipMovement.SetActive(false); ShowPages( "Sehr schön. Dann kann ja jetzt nichts mehr schief gehen!", "Gleich blende ich dir ein Wort auf der Konsole ein und du musst die korrekte Übersetzung finden.", "Vor dir werden die Worte auf der Bahn auftauchen. Fahre einfach über das korrekte Wort rüber.", "Für jede korrekte Antwort gibt es Punkte.", "Sind die Worte zu weit weg, kannst du sie direkt angucken. Dadurch erscheinen sie bei dir größer auf dem Heads Up Display.", "Los geht's!" ); phase = TutorialPhase.ExplainQuestions; } // GateSpawner-Callbacks private void OnTutorialQuestionAnswered(bool _) { if (!tutorialActive) return; tutorialAnswers++; if (tutorialAnswers >= tutorialAnswersTarget) { tutorialActive = false; if (gateSpawner) gateSpawner.StopTutorialQuestions(); if (shipMovement) shipMovement.SetActive(false); ShowPages("Na siehst du, ganz einfach oder?"); phase = TutorialPhase.FinalCongrats; } } private void OnTutorialSequenceFinished() { if (!tutorialActive) return; tutorialActive = false; if (shipMovement) shipMovement.SetActive(false); ShowPages("Wähle nun einen der Spielmodi. Schnell, bevor noch mehr Wörter aus dieser Welt verschwinden!"); phase = TutorialPhase.FinalCongrats; } // ====== Hebel-Puls ====== private void StartLeverPulse() { if (leverPulseActive) return; leverPulseActive = true; if (pulseCo != null) StopCoroutine(pulseCo); pulseCo = StartCoroutine(Co_PulseLever()); } private void StopLeverPulse() { leverPulseActive = false; if (pulseCo != null) { StopCoroutine(pulseCo); pulseCo = null; } RestoreLeverVisuals(); } private System.Collections.IEnumerator Co_PulseLever() { if (leverRenderers == null || leverRenderers.Length == 0) yield break; // Emission-Keyword vorbereiten for (int i = 0; i < leverRenderers.Length; i++) { var r = leverRenderers[i]; if (!r) continue; r.GetPropertyBlock(mpbs[i]); if (r.sharedMaterial && r.sharedMaterial.HasProperty(_EmissionColorID)) r.sharedMaterial.EnableKeyword("_EMISSION"); } float t = 0f; while (leverPulseActive) { t += Time.deltaTime * Mathf.Max(0.01f, pulseSpeed) * Mathf.PI * 2f; float s = (Mathf.Sin(t) * 0.5f + 0.5f); float intensity = Mathf.Lerp(pulseMin, pulseMax, s); Color baseC = pulseColor * intensity; for (int i = 0; i < leverRenderers.Length; i++) { var r = leverRenderers[i]; if (!r) continue; var mpb = mpbs[i]; if (r.sharedMaterial && r.sharedMaterial.HasProperty(_EmissionColorID)) mpb.SetColor(_EmissionColorID, baseC); else if (r.sharedMaterial && r.sharedMaterial.HasProperty(_ColorID)) mpb.SetColor(_ColorID, baseC); r.SetPropertyBlock(mpb); } yield return null; } } private void RestoreLeverVisuals() { if (leverRenderers == null) return; for (int i = 0; i < leverRenderers.Length; i++) { var r = leverRenderers[i]; if (!r) continue; r.SetPropertyBlock(null); } } }