/// <summary>
        /// A coroutine that fades a <see cref="CanvasGroup"/> object's
        /// opacity from <paramref name="from"/> to <paramref name="to"/>
        /// over the course of <see cref="fadeTime"/> seconds, and then
        /// invokes <paramref name="onComplete"/>.
        /// </summary>
        /// <param name="from">The opacity value to start fading from,
        /// ranging from 0 to 1.</param>
        /// <param name="to">The opacity value to end fading at, ranging
        /// from 0 to 1.</param>
        /// <param name="onComplete">A delegate to invoke after fading is
        /// complete.</param>
        public static IEnumerator FadeAlpha(CanvasGroup canvasGroup, float from, float to, float fadeTime, PresentationFlag presented = null, Action onComplete = null)
        {
            canvasGroup.alpha = from;

            var timeElapsed = 0f;

            while (timeElapsed < fadeTime)
            {
                var fraction = timeElapsed / fadeTime;
                timeElapsed += Time.deltaTime;

                float a = Mathf.Lerp(from, to, fraction);

                canvasGroup.alpha = a;
                yield return(null);
            }

            canvasGroup.alpha = to;

            if (to == 0)
            {
                canvasGroup.interactable   = false;
                canvasGroup.blocksRaycasts = false;
            }
            else
            {
                canvasGroup.interactable   = true;
                canvasGroup.blocksRaycasts = true;
            }

            presented?.Set();
            onComplete?.Invoke();
        }
        public static IEnumerator Typewriter(TextMeshProUGUI text, float lettersPerSecond, PresentationFlag presented, Action onCharacterTyped, Action onComplete)
        {
            // Start with everything invisible
            text.maxVisibleCharacters = 0;

            // Wait a single frame to let the text component process its
            // content, otherwise text.textInfo.characterCount won't be
            // accurate
            yield return(null);

            // How many visible characters are present in the text?
            var characterCount = text.textInfo.characterCount;

            // Early out if letter speed is zero or text length is zero
            if (lettersPerSecond <= 0 || characterCount == 0)
            {
                // Show everything and invoke the completion handler
                text.maxVisibleCharacters = characterCount;
                onComplete?.Invoke();
                yield break;
            }

            // Convert 'letters per second' into its inverse
            float secondsPerLetter = 1.0f / lettersPerSecond;

            // If lettersPerSecond is larger than the average framerate, we
            // need to show more than one letter per frame, so simply
            // adding 1 letter every secondsPerLetter won't be good enough
            // (we'd cap out at 1 letter per frame, which could be slower
            // than the user requested.)
            //
            // Instead, we'll accumulate time every frame, and display as
            // many letters in that frame as we need to in order to achieve
            // the requested speed.
            var accumulator = Time.deltaTime;

            while (text.maxVisibleCharacters < characterCount)
            {
                // We need to show as many letters as we have accumulated
                // time for.
                while (accumulator >= secondsPerLetter)
                {
                    text.maxVisibleCharacters += 1;
                    onCharacterTyped?.Invoke();
                    accumulator -= secondsPerLetter;
                }
                accumulator += Time.deltaTime;

                yield return(null);
            }

            // We either finished displaying everything, or were
            // interrupted. Either way, display everything now.
            text.maxVisibleCharacters = characterCount;

            presented?.Set();

            // Wrap up by invoking our completion handler.
            onComplete?.Invoke();
        }
        public override void RunLine(LocalizedLine dialogueLine, Action onDialogueLineFinished)
        {
            currentLine = dialogueLine;
            hasPresented.Clear();

            // if we have auto advance on we just hand over the completion handler
            // if we don't have auto advance we send over no completion handler and store the completion handler
            // this way it can be called later by the user action
            var completionHandler = autoAdvance ? onDialogueLineFinished : null;

            dialogueFinishedAction = onDialogueLineFinished;

            lineText.gameObject.SetActive(true);
            canvasGroup.gameObject.SetActive(true);

            if (continueButton != null)
            {
                continueButton.SetActive(false);
            }

            if (characterNameText == null)
            {
                if (showCharacterNameInLineView)
                {
                    lineText.text = dialogueLine.Text.Text;
                }
                else
                {
                    lineText.text = dialogueLine.TextWithoutCharacterName.Text;
                }
            }
            else
            {
                characterNameText.text = dialogueLine.CharacterName;
                lineText.text          = dialogueLine.TextWithoutCharacterName.Text;
            }

            bool needsHold = autoAdvance && holdTime > 0;

            if (useFadeEffect)
            {
                if (useTypewriterEffect)
                {
                    // If we're also using a typewriter effect, ensure that
                    // there are no visible characters so that we don't
                    // fade in on the text fully visible
                    lineText.maxVisibleCharacters = 0;
                }
                else
                {
                    // Ensure that the max visible characters is effectively unlimited.
                    lineText.maxVisibleCharacters = int.MaxValue;
                }

                // if we are set to auto advance we want to hold for the
                // amount of time set in holdTime before calling the
                // completion handler to continue the dialogue
                if (needsHold)
                {
                    StartCoroutine(Effects.FadeAlpha(canvasGroup, 0, 1, fadeInTime, useTypewriterEffect ? null: hasPresented, () => FadeComplete(() => HoldAndContinue(completionHandler))));
                }
                else
                {
                    // Fade up and then call FadeComplete when done
                    StartCoroutine(Effects.FadeAlpha(canvasGroup, 0, 1, fadeInTime, useTypewriterEffect ? null: hasPresented, () => FadeComplete(completionHandler)));
                }
            }
            else
            {
                // Immediately appear
                canvasGroup.interactable   = true;
                canvasGroup.alpha          = 1;
                canvasGroup.blocksRaycasts = true;

                if (useTypewriterEffect)
                {
                    if (needsHold)
                    {
                        StartCoroutine(Effects.Typewriter(lineText, typewriterEffectSpeed, hasPresented, OnCharacterTyped, () => HoldAndContinue(completionHandler)));
                    }
                    else
                    {
                        StartCoroutine(Effects.Typewriter(lineText, typewriterEffectSpeed, hasPresented, OnCharacterTyped, completionHandler));
                    }
                }
                else
                {
                    if (needsHold)
                    {
                        Action hold = () => {
                            hasPresented.Set();
                            completionHandler();
                        };
                        HoldAndContinue(hold);
                    }
                    else
                    {
                        hasPresented.Set();
                        completionHandler();
                    }
                }
            }

            void FadeComplete(Action onFinished)
            {
                if (useTypewriterEffect)
                {
                    StartCoroutine(Effects.Typewriter(lineText, typewriterEffectSpeed, hasPresented, OnCharacterTyped, onFinished));
                }
                else
                {
                    onFinished();
                }
            }

            void HoldAndContinue(Action onFinished)
            {
                StartCoroutine(DelayAction(holdTime, onFinished));
            }
        }