using System; using UnityEngine; using UnityEngine.Playables; using System.Linq; using System.Threading.Tasks; using Input; namespace Interactions { /// /// Component that plays timeline animations in response to interaction events /// [RequireComponent(typeof(PlayableDirector))] public class InteractionTimelineAction : InteractionActionBase { [System.Serializable] public class TimelineEventMapping { public InteractionEventType eventType; public PlayableAsset[] timelines; [Tooltip("Whether to bind the player character to the track named 'Player'")] public bool bindPlayerCharacter = false; [Tooltip("Whether to bind the follower character to the track named 'Pulver'")] public bool bindPulverCharacter = false; [Tooltip("Custom track name for player character binding")] public string playerTrackName = "Player"; [Tooltip("Custom track name for follower character binding")] public string pulverTrackName = "Pulver"; [Tooltip("Time in seconds before the timeline is automatically completed (safety feature)")] public float timeoutSeconds = 30f; [Tooltip("Whether to loop the last timeline in the sequence")] public bool loopLast = false; [Tooltip("Whether to loop through all timelines in the sequence")] public bool loopAll = false; // Helper property to check if we have valid timelines public bool HasValidTimelines => timelines != null && timelines.Length > 0 && timelines[0] != null; } [Header("Timeline Configuration")] [SerializeField] private PlayableDirector playableDirector; [SerializeField] private TimelineEventMapping[] timelineMappings; private TaskCompletionSource _currentPlaybackTCS; private int _currentTimelineIndex = 0; private TimelineEventMapping _currentMapping = null; protected override void Awake() { base.Awake(); if (playableDirector == null) { playableDirector = GetComponent(); } if (playableDirector == null) { Debug.LogError("[InteractionTimelineAction] PlayableDirector component is missing!"); enabled = false; return; } // Subscribe to the director's stopped event playableDirector.stopped += OnPlayableDirectorStopped; } private void OnDestroy() { if (playableDirector != null) { playableDirector.stopped -= OnPlayableDirectorStopped; } } protected override async Task ExecuteAsync(InteractionEventType eventType, PlayerTouchController player, FollowerController follower) { // Find the timeline for this event type TimelineEventMapping mapping = Array.Find(timelineMappings, m => m.eventType == eventType); if (mapping == null || !mapping.HasValidTimelines) { // No timeline configured for this event return true; } _currentMapping = mapping; // _currentTimelineIndex = 0; return await PlayTimelineSequence(player, follower); } private async Task PlayTimelineSequence(PlayerTouchController player, FollowerController follower) { if (_currentMapping == null || !_currentMapping.HasValidTimelines) { return true; } follower.DropHeldItemAt(follower.transform.position); // Play the current timeline in the sequence bool result = await PlaySingleTimeline(_currentMapping.timelines[_currentTimelineIndex], _currentMapping, player, follower); // Return false if the playback failed if (!result) { return false; } // Increment the timeline index for next playback _currentTimelineIndex++; // Check if we've reached the end of the sequence if (_currentTimelineIndex >= _currentMapping.timelines.Length) { // If loop all is enabled, start over if (_currentMapping.loopAll) { _currentTimelineIndex = 0; // Don't continue automatically, wait for next interaction return true; } // If loop last is enabled, replay the last timeline else if (_currentMapping.loopLast) { _currentTimelineIndex = _currentMapping.timelines.Length - 1; // Don't continue automatically, wait for next interaction return true; } // Otherwise, we're done with the sequence else { _currentTimelineIndex = 0; _currentMapping = null; return true; } } // If we have more timelines in the sequence, we're done for now // Next interaction will pick up where we left off return true; } private async Task PlaySingleTimeline(PlayableAsset timelineAsset, TimelineEventMapping mapping, PlayerTouchController player, FollowerController follower) { if (timelineAsset == null) { Debug.LogWarning("[InteractionTimelineAction] Timeline asset is null"); return true; // Return true to continue the interaction flow } // Set the timeline asset playableDirector.playableAsset = timelineAsset; // Bind characters if needed if (mapping.bindPlayerCharacter && player != null) { try { var trackOutput = playableDirector.playableAsset.outputs.FirstOrDefault(o => o.streamName == mapping.playerTrackName); if (trackOutput.sourceObject != null) { playableDirector.SetGenericBinding(trackOutput.sourceObject, player.gameObject); } else { Debug.LogWarning($"[InteractionTimelineAction] Could not find track named '{mapping.playerTrackName}' for player binding"); } } catch (Exception ex) { Debug.LogError($"[InteractionTimelineAction] Error binding player to timeline: {ex.Message}"); } } if (mapping.bindPulverCharacter && follower != null) { try { var trackOutput = playableDirector.playableAsset.outputs.FirstOrDefault(o => o.streamName == mapping.pulverTrackName); if (trackOutput.sourceObject != null) { playableDirector.SetGenericBinding(trackOutput.sourceObject, follower.gameObject); } else { Debug.LogWarning($"[InteractionTimelineAction] Could not find track named '{mapping.pulverTrackName}' for follower binding"); } } catch (Exception ex) { Debug.LogError($"[InteractionTimelineAction] Error binding follower to timeline: {ex.Message}"); } } // Create a task completion source to await the timeline completion _currentPlaybackTCS = new TaskCompletionSource(); // Register for the stopped event if not already registered playableDirector.stopped -= OnPlayableDirectorStopped; playableDirector.stopped += OnPlayableDirectorStopped; // Log the timeline playback Debug.Log($"[InteractionTimelineAction] Playing timeline {timelineAsset.name} for event {mapping.eventType}"); // Play the timeline playableDirector.Play(); // Start a timeout coroutine for safety using the mapping's timeout StartCoroutine(TimeoutCoroutine(mapping.timeoutSeconds)); // Await the timeline completion (will be signaled by the OnPlayableDirectorStopped callback) bool result = await _currentPlaybackTCS.Task; // Log completion Debug.Log($"[InteractionTimelineAction] Timeline {timelineAsset.name} playback completed with result: {result}"); // Clear the task completion source _currentPlaybackTCS = null; return result; } private void OnPlayableDirectorStopped(PlayableDirector director) { if (director != playableDirector || _currentPlaybackTCS == null) return; Debug.Log($"[InteractionTimelineAction] PlayableDirector stopped. Signaling completion."); // Signal completion when the director stops _currentPlaybackTCS.TrySetResult(true); } private System.Collections.IEnumerator TimeoutCoroutine(float timeoutDuration) { yield return new WaitForSeconds(timeoutDuration); // If the TCS still exists after timeout, complete it with failure if (_currentPlaybackTCS != null) { Debug.LogWarning($"[InteractionTimelineAction] Timeline playback timed out after {timeoutDuration} seconds"); _currentPlaybackTCS.TrySetResult(false); } } } }