SpaceQuestions_MarioKart/Assets/Scripts/TutorialFlightController.cs
2025-12-11 13:14:43 +01:00

415 lines
13 KiB
C#

// 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<string> _currentPages = new List<string>();
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<Renderer>(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 <b>Tutorial</b>! Hier lernst du Schritt für Schritt, wie du das Schiff steuerst.",
"Um das Schiff zu starten, greife nach dem <b>Hebel</b> rechts vor dir.",
"Sobald dein rechter Controller den Hebel berührt, halte den <b>rechten Trigger</b> 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 <b>neigst</b> 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 <b>schief</b> gehen!",
"Gleich blende ich dir ein Wort auf der <b>Konsole</b> ein und du musst die korrekte Übersetzung finden.",
"Vor dir werden die Worte auf der Bahn auftauchen. Fahre einfach <b>über</b> das korrekte Wort <b>rüber</b>.",
"Für jede korrekte Antwort gibt es <b>Punkte</b>.",
"Sind die Worte zu weit weg, kannst du sie direkt <b>angucken</b>. Dadurch erscheinen sie bei dir größer auf dem <b>Heads Up Display</b>.",
"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 <b>verschwinden</b>!");
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);
}
}
}