using System.Collections; using UnityEngine; using UnityEngine.Playables; using Yarn.Unity; namespace AibisDream { /// /// Drives the deliberately unsuccessful focus pulls at the start of the /// starfield-to-sea Timeline. The effect is evaluated from Timeline time so /// pausing, frame drops and repeated playback remain deterministic. /// public sealed class StarfieldDefocusController : MonoBehaviour { private static readonly int BlurAmountId = Shader.PropertyToID("_BlurAmount"); private static readonly int BlurSizeId = Shader.PropertyToID("_BlurSize"); private static readonly int UseSoftFocusId = Shader.PropertyToID("_UseSoftFocus"); [Header("References")] [SerializeField] private PlayableDirector director; [SerializeField] private SpriteRenderer starfieldRenderer; [Header("Defocus Look")] [Range(0f, 1f)] [SerializeField] private float initialBlur = 0.78f; [Range(0f, 0.05f)] [SerializeField] private float blurSize = 0.009f; [Header("Focus Attempt 1")] [Min(0f)] [SerializeField] private float firstAttemptStart = 0.45f; [Min(0.01f)] [SerializeField] private float firstFocusDuration = 0.48f; [Range(0f, 1f)] [SerializeField] private float firstNearFocusBlur = 0.38f; [Min(0f)] [SerializeField] private float firstHoldDuration = 0.14f; [Min(0.01f)] [SerializeField] private float firstSlipDuration = 0.38f; [Range(0f, 1f)] [SerializeField] private float firstSlipBlur = 0.72f; [Header("Focus Attempt 2")] [Min(0f)] [SerializeField] private float secondAttemptStart = 1.72f; [Min(0.01f)] [SerializeField] private float secondFocusDuration = 0.55f; [Range(0f, 1f)] [SerializeField] private float secondNearFocusBlur = 0.28f; [Min(0f)] [SerializeField] private float secondHoldDuration = 0.18f; [Min(0.01f)] [SerializeField] private float secondSlipDuration = 0.46f; [Range(0f, 1f)] [SerializeField] private float unresolvedBlur = 0.62f; [Header("Downward Pan")] [Tooltip("Timeline time at which the starfield starts moving down toward the sea.")] [Min(0f)] [SerializeField] private float blurFadeStart = 4f; [Min(0.01f)] [SerializeField] private float blurFadeDuration = 3.2f; private MaterialPropertyBlock propertyBlock; [YarnCommand("starfield_prepare")] public static void PrepareStarfieldCommand() { var controller = FindFirstObjectByType(); if (controller == null) { Debug.LogWarning("[StarfieldDefocusController] No active controller found for starfield_prepare."); return; } controller.PrepareStarfield(); } [YarnCommand("starfield_focus")] public static IEnumerator PlayFocusAttemptCommand(int attempt) { var controller = FindFirstObjectByType(); if (controller == null) { Debug.LogWarning("[StarfieldDefocusController] No active controller found for starfield_focus."); yield break; } yield return controller.PlayFocusAttempt(attempt); } [YarnCommand("starfield_descend")] public static IEnumerator DescendToSeaCommand() { var controller = FindFirstObjectByType(); if (controller == null) { Debug.LogWarning("[StarfieldDefocusController] No active controller found for starfield_descend."); yield break; } yield return controller.DescendToSea(); } private void PrepareStarfield() { if (!TryValidateReferences()) { return; } director.Stop(); SetTimelineTime(0f); } private IEnumerator PlayFocusAttempt(int attempt) { if (!TryValidateReferences()) { yield break; } float startTime; float endTime; switch (attempt) { case 1: startTime = firstAttemptStart; endTime = firstAttemptStart + firstFocusDuration + firstHoldDuration + firstSlipDuration; break; case 2: startTime = secondAttemptStart; endTime = secondAttemptStart + secondFocusDuration + secondHoldDuration + secondSlipDuration; break; default: Debug.LogWarning($"[StarfieldDefocusController] Unsupported focus attempt '{attempt}'. Expected 1 or 2.", this); yield break; } director.Pause(); SetTimelineTime(startTime); float duration = Mathf.Max(0.01f, endTime - startTime); float elapsed = 0f; while (elapsed < duration) { elapsed += Time.deltaTime; SetTimelineTime(Mathf.Lerp(startTime, endTime, Mathf.Clamp01(elapsed / duration))); yield return null; } SetTimelineTime(endTime); } private IEnumerator DescendToSea() { if (!TryValidateReferences()) { yield break; } director.Pause(); SetTimelineTime(blurFadeStart); director.Play(); while (director.state == PlayState.Playing) { yield return null; } ApplyAtTime((float)director.time); } private void Reset() { director = GetComponent(); starfieldRenderer = GetComponentInChildren(true); } private void Awake() { propertyBlock = new MaterialPropertyBlock(); ApplyAtTime(director != null ? (float)director.time : 0f); } private void OnEnable() { ApplyAtTime(director != null ? (float)director.time : 0f); } private void LateUpdate() { if (director == null || starfieldRenderer == null) { return; } ApplyAtTime((float)director.time); } private bool TryValidateReferences() { if (director == null) { Debug.LogWarning("[StarfieldDefocusController] PlayableDirector is not assigned.", this); return false; } if (director.playableAsset == null) { Debug.LogWarning("[StarfieldDefocusController] PlayableDirector has no Timeline asset.", this); return false; } if (starfieldRenderer == null) { Debug.LogWarning("[StarfieldDefocusController] Starfield SpriteRenderer is not assigned.", this); return false; } var material = starfieldRenderer.sharedMaterial; if (material == null || !material.HasProperty(BlurAmountId) || !material.HasProperty(BlurSizeId)) { Debug.LogWarning( "[StarfieldDefocusController] Starfield material must support _BlurAmount and _BlurSize.", starfieldRenderer); return false; } return true; } private void SetTimelineTime(float time) { director.time = Mathf.Clamp(time, 0f, (float)director.duration); director.Evaluate(); ApplyAtTime((float)director.time); } private void ApplyAtTime(float time) { if (starfieldRenderer == null) { return; } propertyBlock ??= new MaterialPropertyBlock(); starfieldRenderer.GetPropertyBlock(propertyBlock); propertyBlock.SetFloat(BlurAmountId, EvaluateBlur(time)); propertyBlock.SetFloat(BlurSizeId, blurSize); propertyBlock.SetFloat(UseSoftFocusId, 1f); starfieldRenderer.SetPropertyBlock(propertyBlock); } private float EvaluateBlur(float time) { float firstFocusEnd = firstAttemptStart + firstFocusDuration; float firstHoldEnd = firstFocusEnd + firstHoldDuration; float firstSlipEnd = firstHoldEnd + firstSlipDuration; float secondFocusEnd = secondAttemptStart + secondFocusDuration; float secondHoldEnd = secondFocusEnd + secondHoldDuration; float secondSlipEnd = secondHoldEnd + secondSlipDuration; if (time < firstAttemptStart) { return initialBlur; } if (time < firstFocusEnd) { return EvaluateFocusTurn( initialBlur, firstNearFocusBlur, InverseLerp(firstAttemptStart, firstFocusEnd, time)); } if (time < firstHoldEnd) { return firstNearFocusBlur; } if (time < firstSlipEnd) { return SmoothLerp(firstNearFocusBlur, firstSlipBlur, InverseLerp(firstHoldEnd, firstSlipEnd, time)); } if (time < secondAttemptStart) { return firstSlipBlur; } if (time < secondFocusEnd) { return EvaluateFocusTurn( firstSlipBlur, secondNearFocusBlur, InverseLerp(secondAttemptStart, secondFocusEnd, time)); } if (time < secondHoldEnd) { return secondNearFocusBlur; } if (time < secondSlipEnd) { return SmoothLerp(secondNearFocusBlur, unresolvedBlur, InverseLerp(secondHoldEnd, secondSlipEnd, time)); } if (time < blurFadeStart) { return unresolvedBlur; } return SmoothLerp(unresolvedBlur, 0f, InverseLerp(blurFadeStart, blurFadeStart + blurFadeDuration, time)); } /// /// Simulates turning a focus ring toward focus, passing the focal plane, /// then reversing back. The following slip phase still lets the image /// drift out of focus, keeping both attempts deliberately unsuccessful. /// private static float EvaluateFocusTurn(float fromBlur, float nearFocusBlur, float t) { t = Mathf.Clamp01(t); float overshootBlur = Mathf.Lerp(nearFocusBlur, fromBlur, 0.68f); if (t < 0.44f) { return SmoothLerp(fromBlur, nearFocusBlur, t / 0.44f); } if (t < 0.72f) { return SmoothLerp(nearFocusBlur, overshootBlur, (t - 0.44f) / 0.28f); } return SmoothLerp(overshootBlur, nearFocusBlur, (t - 0.72f) / 0.28f); } private static float InverseLerp(float from, float to, float value) { return Mathf.Clamp01((value - from) / Mathf.Max(0.0001f, to - from)); } private static float SmoothLerp(float from, float to, float t) { t = Mathf.Clamp01(t); t = t * t * (3f - 2f * t); return Mathf.LerpUnclamped(from, to, t); } } }