using System; using System.Collections; using UnityEngine; using UnityEngine.UI; using UnityEngine.Video; using OctoCore.Transitions; using Unheimlich.Core; namespace Unheimlich.Transitions { /// /// Persistent singleton engine for "eyelid blink" transitions (eyelids + optional video overlay). /// Lives outside the OctoCore package (this project already references LeanTween here), and /// implements IOctoSceneTransition so OctoNavigation can drive it without the package ever /// depending on LeanTween or this concrete type. Registers itself with OctoTransitionProvider /// once ready so the package can find it at runtime. /// public class OctoEyelidBlink : Singleton, IOctoSceneTransition { [Header("Eyelids")] [SerializeField] private Image _topEyelid; [SerializeField] private Image _bottomEyelid; [Header("Video")] [Tooltip("RawImage displaying the shared RenderTexture. Its material is swapped per call.")] [SerializeField] private RawImage _videoDisplay; [SerializeField] private VideoPlayer _videoPlayer; [SerializeField] private bool _skipOnDrop = true; [SerializeField] private bool _logVideoDebug = false; [Header("Positions")] [SerializeField] private float _openOffset = 600f; [SerializeField] private float _closedOffset = 0f; /// True while a blink sequence is currently animating. public bool IsBusy { get; private set; } private int _topTweenId = -1; private int _bottomTweenId = -1; private int _delayTweenId = -1; private int _videoTweenId = -1; private bool _isWarmedUp; private bool _videoShown; private VideoClip _pendingClip; private Material _defaultMaterial; private bool HasVideo => _videoPlayer != null && _videoDisplay != null && _videoPlayer.clip != null; protected override void OnAwake() { OctoTransitionProvider.Current = this; // Only call DontDestroyOnLoad on a root object. If this component already lives // under the persistent Octo prefab, the parent handles persistence and this is a no-op. if (transform.parent == null) DontDestroyOnLoad(gameObject); if (_videoDisplay != null) _defaultMaterial = _videoDisplay.material; SetEyelidsOpen(); SetVideoVisible(false); ConfigureVideoPlayer(); } // Re-registers with the transition provider whenever Instance is restored // (e.g. after a SubsystemRegistration reset with Domain Reload disabled). protected override void OnEnabled() { OctoTransitionProvider.Current = this; } protected override void OnDestroyInstance() { if (OctoTransitionProvider.Current == (IOctoSceneTransition)this) OctoTransitionProvider.Current = null; } // ---------- IOctoSceneTransition ---------- /// /// Assigns the video clip and optional material to use during the next CloseEyes() call /// and starts warming the clip up. Pass a null clip to perform a plain blink with no video. /// A null material falls back to the RawImage's default material. /// public void SetClip(VideoClip clip, Material material) { if (_videoDisplay != null) _videoDisplay.material = material != null ? material : _defaultMaterial; if (_videoPlayer == null) return; _isWarmedUp = false; _pendingClip = clip; _videoPlayer.clip = clip; if (clip != null) StartCoroutine(WarmUpVideo()); } /// Closes the eyelids over the given duration and invokes the callback on completion. public void CloseEyes(float duration, Action onComplete) { IsBusy = true; CancelCurrent(); ResetVideoToStart(); _videoShown = false; RectTransform top = _topEyelid.rectTransform; RectTransform bottom = _bottomEyelid.rectTransform; _topTweenId = LeanTween.moveY(top, _closedOffset, duration) .setEase(LeanTweenType.easeOutQuad) .setOnComplete(() => { _topTweenId = -1; onComplete?.Invoke(); }) .id; _bottomTweenId = LeanTween.moveY(bottom, -_closedOffset, duration) .setEase(LeanTweenType.easeOutQuad) .setOnComplete(() => _bottomTweenId = -1) .id; } /// Opens the eyelids over the given duration and invokes the callback on completion. public void OpenEyes(float duration, Action onComplete) { RectTransform top = _topEyelid.rectTransform; RectTransform bottom = _bottomEyelid.rectTransform; _topTweenId = LeanTween.moveY(top, _openOffset, duration) .setEase(LeanTweenType.easeInQuad) .setOnComplete(() => { _topTweenId = -1; onComplete?.Invoke(); }) .id; _bottomTweenId = LeanTween.moveY(bottom, -_openOffset, duration) .setEase(LeanTweenType.easeInQuad) .setOnComplete(() => { _bottomTweenId = -1; IsBusy = false; }) .id; } /// Stops playback, resets the video to its first frame, and hides the overlay. public void HideVideo() { if (!HasVideo) return; _videoPlayer.Pause(); _videoPlayer.time = 0.0; SetVideoVisible(false); _videoShown = false; } /// Reveals and plays the video overlay after the given delay, once warm-up has completed. public void ShowVideoAfterDelay(float delay) { if (!HasVideo) return; _videoTweenId = LeanTween.delayedCall(gameObject, delay, () => { _videoTweenId = -1; ShowVideo(); }).id; } // ---------- Extra API (not part of the interface, used by HallucinationFlash directly) ---------- /// Waits for the given duration before invoking the callback. public void Hold(float duration, Action onComplete) { _delayTweenId = LeanTween.delayedCall(gameObject, duration, () => { _delayTweenId = -1; onComplete?.Invoke(); }).id; } // ---------- Video internals ---------- private void ConfigureVideoPlayer() { if (_videoPlayer == null) return; _videoPlayer.playOnAwake = false; _videoPlayer.isLooping = false; _videoPlayer.waitForFirstFrame = true; _videoPlayer.skipOnDrop = _skipOnDrop; } /// /// Prepares and briefly plays the assigned clip while hidden, forcing Unity to /// decode the first frames ahead of time. Prevents a blank/garbage frame from /// being visible the first time the video is actually shown. /// private IEnumerator WarmUpVideo() { if (!HasVideo) yield break; SetVideoVisible(false); ConfigureVideoPlayer(); _videoPlayer.Prepare(); while (!_videoPlayer.isPrepared) yield return null; _videoPlayer.Play(); while (!_videoPlayer.isPlaying) yield return null; // Wait a few frames so the RenderTexture actually receives video data. yield return null; yield return null; yield return null; _videoPlayer.Pause(); _videoPlayer.time = 0.0; // Wait one additional frame after seeking back to the start. yield return null; _isWarmedUp = true; if (_logVideoDebug) Debug.Log($"[OctoEyelidBlink] Warm-up complete for clip: {_pendingClip?.name}"); } private void ShowVideo() { if (!HasVideo || _videoShown) return; // Safety check: if warm-up hasn't completed yet (e.g. clip assigned too late), // skip showing the video rather than risk a corrupted first frame. The eyelid // blink itself remains unaffected. if (!_isWarmedUp) { if (_logVideoDebug) Debug.LogWarning("[OctoEyelidBlink] Video not warmed up in time, skipping display."); return; } _videoShown = true; SetVideoVisible(true); _videoPlayer.Play(); } private void ResetVideoToStart() { if (!HasVideo) return; _videoPlayer.Pause(); _videoPlayer.time = 0.0; } private void SetVideoVisible(bool visible) { if (_videoDisplay != null) _videoDisplay.enabled = visible; } // ---------- Eyelid internals ---------- private void SetEyelidsOpen() { _topEyelid.rectTransform.anchoredPosition = new Vector2(_topEyelid.rectTransform.anchoredPosition.x, _openOffset); _bottomEyelid.rectTransform.anchoredPosition = new Vector2(_bottomEyelid.rectTransform.anchoredPosition.x, -_openOffset); } private void CancelCurrent() { if (_topTweenId != -1) { LeanTween.cancel(_topTweenId); _topTweenId = -1; } if (_bottomTweenId != -1) { LeanTween.cancel(_bottomTweenId); _bottomTweenId = -1; } if (_delayTweenId != -1) { LeanTween.cancel(_delayTweenId); _delayTweenId = -1; } if (_videoTweenId != -1) { LeanTween.cancel(_videoTweenId); _videoTweenId = -1; } } } }