using System.Collections; using UnityEngine; using UnityEngine.UI; using UnityEngine.Video; namespace Unheimlich.Features.Hallucination { /// /// Plays a short hallucination video using an eye blink transition on the HUD canvas. /// Movement is handled via LeanTween. /// Call Play() to trigger the hallucination. /// public class HallucinationFlash : MonoBehaviour { [Header("Target")] [SerializeField] private VideoPlayer _videoPlayer; [SerializeField] private RawImage _hallucinationVideo; [SerializeField] private Image _topEyelid; [SerializeField] private Image _bottomEyelid; [Header("Timings")] [SerializeField, Min(0f)] private float _closeDuration = 0.1f; [SerializeField, Min(0f)] private float _holdClosed = 0.05f; [SerializeField, Min(0f)] private float _openDuration = 0.15f; [SerializeField, Min(0f)] private float _visibleHold = 0.5f; [Header("Video Timing")] [SerializeField, Range(0f, 1f)] private float _showVideoAtCloseProgress = 0.25f; [Header("Video Playback")] [SerializeField, Min(0f)] private float _playbackSpeed = 1f; [SerializeField] private bool _skipOnDrop = true; [SerializeField] private bool _logVideoDebug = false; [Header("Positions")] [SerializeField] private float _openOffset = 600f; [SerializeField] private float _closedOffset = 0f; private int _topTweenId = -1; private int _bottomTweenId = -1; private int _delayTweenId = -1; private int _videoTweenId = -1; private bool _isWarmedUp; private bool _playWhenReady; private bool _videoShown; private void Awake() { SetHallucinationVisible(false); SetEyelidsOpen(); ConfigureVideoPlayer(); } private void Start() { StartCoroutine(WarmUpVideo()); } public void Play() { if (!_isWarmedUp) { _playWhenReady = true; return; } PlayPreparedSequence(); } private void ConfigureVideoPlayer() { if (_videoPlayer == null) return; _videoPlayer.playOnAwake = false; _videoPlayer.isLooping = false; _videoPlayer.waitForFirstFrame = true; _videoPlayer.skipOnDrop = _skipOnDrop; _videoPlayer.playbackSpeed = _playbackSpeed; if (_logVideoDebug) { Debug.Log( "[HallucinationFlash] ConfigureVideoPlayer" + " | speed=" + _videoPlayer.playbackSpeed + " | canSetSpeed=" + _videoPlayer.canSetPlaybackSpeed + " | skipOnDrop=" + _videoPlayer.skipOnDrop + " | updateMode=" + _videoPlayer.timeUpdateMode ); } } private IEnumerator WarmUpVideo() { if (_videoPlayer == null) yield break; SetHallucinationVisible(false); ConfigureVideoPlayer(); _videoPlayer.Prepare(); while (!_videoPlayer.isPrepared) yield return null; ConfigureVideoPlayer(); // Play once while hidden to force Unity to decode the first frames. _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; ConfigureVideoPlayer(); // Wait one frame after seeking back to the beginning. yield return null; _isWarmedUp = true; if (!_playWhenReady) yield break; _playWhenReady = false; PlayPreparedSequence(); } private void PlayPreparedSequence() { CancelCurrent(); ResetVideoToStart(); SetHallucinationVisible(false); SetEyelidsOpen(); _videoShown = false; CloseEyes(() => { if (!_videoShown) ShowVideo(); HoldClosed(() => { OpenEyes(() => { HoldVisible(() => { CloseEyes(() => { HideVideo(); HoldClosed(() => { OpenEyes(() => { ClearTweenIds(); }); }); }); }); }); }); }); ShowVideoDuringClose(); } private void CloseEyes(System.Action onComplete) { RectTransform top = _topEyelid.rectTransform; RectTransform bottom = _bottomEyelid.rectTransform; _topTweenId = LeanTween.moveY(top, _closedOffset, _closeDuration) .setEase(LeanTweenType.easeOutQuad) .setOnComplete(() => { _topTweenId = -1; onComplete?.Invoke(); }) .id; _bottomTweenId = LeanTween.moveY(bottom, -_closedOffset, _closeDuration) .setEase(LeanTweenType.easeOutQuad) .setOnComplete(() => { _bottomTweenId = -1; }) .id; } private void OpenEyes(System.Action onComplete) { RectTransform top = _topEyelid.rectTransform; RectTransform bottom = _bottomEyelid.rectTransform; _topTweenId = LeanTween.moveY(top, _openOffset, _openDuration) .setEase(LeanTweenType.easeInQuad) .setOnComplete(() => { _topTweenId = -1; onComplete?.Invoke(); }) .id; _bottomTweenId = LeanTween.moveY(bottom, -_openOffset, _openDuration) .setEase(LeanTweenType.easeInQuad) .setOnComplete(() => { _bottomTweenId = -1; }) .id; } private void ShowVideoDuringClose() { float delay = _closeDuration * _showVideoAtCloseProgress; _videoTweenId = LeanTween.delayedCall(gameObject, delay, () => { _videoTweenId = -1; ShowVideo(); }).id; } private void ShowVideo() { if (_videoShown) return; _videoShown = true; SetHallucinationVisible(true); PlayVideo(); } private void HideVideo() { PauseVideoAtStart(); SetHallucinationVisible(false); } private void HoldClosed(System.Action onComplete) { _delayTweenId = LeanTween.delayedCall(gameObject, _holdClosed, () => { _delayTweenId = -1; onComplete?.Invoke(); }).id; } private void HoldVisible(System.Action onComplete) { _delayTweenId = LeanTween.delayedCall(gameObject, _visibleHold, () => { _delayTweenId = -1; onComplete?.Invoke(); }).id; } private void PlayVideo() { if (_videoPlayer == null) return; ConfigureVideoPlayer(); // Do not Stop() here. Stop() can reintroduce decoding latency. _videoPlayer.Play(); if (_logVideoDebug) { Debug.Log( "[HallucinationFlash] PlayVideo" + " | speed=" + _videoPlayer.playbackSpeed + " | canSetSpeed=" + _videoPlayer.canSetPlaybackSpeed + " | time=" + _videoPlayer.time + " | frame=" + _videoPlayer.frame ); } } private void ResetVideoToStart() { if (_videoPlayer == null) return; _videoPlayer.Pause(); _videoPlayer.time = 0.0; ConfigureVideoPlayer(); } private void PauseVideoAtStart() { if (_videoPlayer == null) return; _videoPlayer.Pause(); _videoPlayer.time = 0.0; ConfigureVideoPlayer(); } private void SetHallucinationVisible(bool visible) { if (_hallucinationVideo == null) return; _hallucinationVideo.enabled = visible; } private void SetEyelidsOpen() { if (_topEyelid == null || _bottomEyelid == null) return; RectTransform top = _topEyelid.rectTransform; RectTransform bottom = _bottomEyelid.rectTransform; top.anchoredPosition = new Vector2(top.anchoredPosition.x, _openOffset); bottom.anchoredPosition = new Vector2(bottom.anchoredPosition.x, -_openOffset); } private void CancelCurrent() { _playWhenReady = false; _videoShown = false; 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; } } private void ClearTweenIds() { _topTweenId = -1; _bottomTweenId = -1; _delayTweenId = -1; _videoTweenId = -1; } } }