using System.Collections; using System.Collections.Generic; using System.Linq; using PixelCrushers; // For Saver using PixelCrushers.DialogueSystem; // REQUIRED: Added for DialogueManager events using Sirenix.OdinInspector; using UnityEngine; using Random = UnityEngine.Random; namespace Beyond { [RequireComponent(typeof(Collider))] public class BarkPlayer : Saver { #region Inspector Fields [Header("Bark Configuration")] [Tooltip("The indices of the BarkEntries in BarkManager's list to play sequentially.")] public int[] barkManagerEntryIndices = new int[0]; [Tooltip("Delay in seconds after one bark's audio finishes before starting the next bark WITHIN THE SAME entry sequence.")] [Min(0f)] public float delayBetweenBarks = 0.5f; [Tooltip("Delay in seconds after ALL barks in one entry sequence finish before starting the NEXT entry sequence.")] [Min(0f)] public float delayBetweenEntries = 5.0f; [Tooltip("Play barks within each entry in a random order (shuffled when the entry starts).")] public bool shuffleOrder = false; [Header("Dialogue Integration")] // --- NEW SECTION --- [Tooltip("How long to wait after a dialogue ends before resuming barks.")] public float resumeDelayAfterDialogue = 2.0f; [Header("Trigger Settings")] [Tooltip("Layers that can activate this bark trigger.")] public LayerMask triggeringLayers; [Header("Activation")] [Tooltip("If true, the BarkPlayer will be active automatically when the game starts or when loaded without existing save data for it.")] public bool startAutomaticallyOnLoad = false; [Header("Runtime State (Read Only)")] [SerializeField, ReadOnly] private bool _runtime_IsStarted = false; [SerializeField, ReadOnly] private int currentEntryArrayIndex = 0; [SerializeField, ReadOnly] private int nextBarkSequenceIndex = 0; [SerializeField, ReadOnly] private bool isTriggeringObjectInside = false; #endregion #region Internal Variables private List playbackOrder; private bool isInitialized = false; private Collider triggerCollider; private BarkManager barkManager; private Coroutine currentPlaybackCoroutine = null; private Coroutine resumeCoroutine = null; // To track the resume timer private Transform currentTriggererTransform = null; #endregion #region Save Data Structure [System.Serializable] public class BarkPlayerData { public bool savedIsStarted; public int savedCurrentEntryArrayIndex; public int savedNextBarkSequenceIndex; public bool wasShuffled; } private BarkPlayerData m_saveData = new BarkPlayerData(); #endregion #region Unity Lifecycle Methods public override void Awake() { _runtime_IsStarted = startAutomaticallyOnLoad; base.Awake(); triggerCollider = GetComponent(); if (!triggerCollider.isTrigger) { triggerCollider.isTrigger = true; } if (triggeringLayers.value == 0) { triggeringLayers = LayerMask.GetMask("Player"); } } public override void OnEnable() { base.OnEnable(); // Subscribe to global Dialogue System events // We use the C# event delegates instead of the component message system for reliability if (DialogueManager.instance != null) { DialogueManager.instance.conversationStarted += OnDialogueStarted; DialogueManager.instance.conversationEnded += OnDialogueEnded; } } public override void OnDisable() { base.OnDisable(); // Unsubscribe to prevent memory leaks if (DialogueManager.instance != null) { DialogueManager.instance.conversationStarted -= OnDialogueStarted; DialogueManager.instance.conversationEnded -= OnDialogueEnded; } } private void Start() { barkManager = BarkManager.Instance; if (barkManager == null) { enabled = false; return; } currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1)); if (barkManagerEntryIndices == null || barkManagerEntryIndices.Length == 0) { enabled = false; return; } isInitialized = true; CheckForAutoStartIfInside(); } private void OnDrawGizmos() { Collider col = GetComponent(); if (col != null) { Gizmos.color = isTriggeringObjectInside ? new Color(1f, 0.5f, 0.5f, 0.4f) : new Color(0.5f, 1f, 0.5f, 0.3f); if (col is BoxCollider box) Gizmos.DrawCube(transform.TransformPoint(box.center), Vector3.Scale(box.size, transform.lossyScale)); else if (col is SphereCollider sphere) Gizmos.DrawSphere(transform.TransformPoint(sphere.center), sphere.radius * MaxComponent(transform.lossyScale)); } } #endregion #region Dialogue System Event Handlers (New Logic) // Called automatically by Dialogue System whenever ANY conversation starts private void OnDialogueStarted(Transform actor) { if (resumeCoroutine != null) StopCoroutine(resumeCoroutine); // Halt the bark loop immediately if (currentPlaybackCoroutine != null) { Debug.Log($"BarkPlayer ({gameObject.name}): Dialogue started. Halting barks.", this); StopCoroutine(currentPlaybackCoroutine); currentPlaybackCoroutine = null; } } // Called automatically by Dialogue System whenever ANY conversation ends private void OnDialogueEnded(Transform actor) { // Don't resume if we aren't supposed to be running or nobody is inside if (_runtime_IsStarted && isTriggeringObjectInside && enabled) { Debug.Log($"BarkPlayer ({gameObject.name}): Dialogue ended. Resuming in {resumeDelayAfterDialogue}s.", this); resumeCoroutine = StartCoroutine(ResumeAfterDialogueDelay()); } } private IEnumerator ResumeAfterDialogueDelay() { yield return new WaitForSeconds(resumeDelayAfterDialogue); // Check conditions again in case player left during the wait CheckForAutoStartIfInside(); resumeCoroutine = null; } #endregion #region Trigger Handling private void OnTriggerEnter(Collider other) { if (!isInitialized || !enabled) return; int otherLayer = other.gameObject.layer; if ((triggeringLayers.value & (1 << otherLayer)) == 0) return; if (isTriggeringObjectInside && other.transform != currentTriggererTransform) return; if (isTriggeringObjectInside) return; isTriggeringObjectInside = true; currentTriggererTransform = other.transform; // Added check: Don't start barking if a conversation is ALREADY active if (DialogueManager.isConversationActive) { Debug.Log($"BarkPlayer ({gameObject.name}): Trigger entered, but dialogue is active. Waiting for dialogue end.", this); return; } if (_runtime_IsStarted && currentPlaybackCoroutine == null) { currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1)); currentPlaybackCoroutine = StartCoroutine(ContinuousPlaybackLoop()); } } private void OnTriggerExit(Collider other) { if (!isInitialized || !enabled) return; int otherLayer = other.gameObject.layer; if ((triggeringLayers.value & (1 << otherLayer)) == 0) return; if (other.transform == currentTriggererTransform) { isTriggeringObjectInside = false; currentTriggererTransform = null; if (resumeCoroutine != null) StopCoroutine(resumeCoroutine); if (currentPlaybackCoroutine != null) { StopCoroutine(currentPlaybackCoroutine); currentPlaybackCoroutine = null; } } } #endregion #region Playback Logic private bool PrepareEntrySequence(int entryIndexToPrepare) { if (barkManager == null) return false; int barkCount = barkManager.GetBarkCountInEntry(entryIndexToPrepare); if (barkCount <= 0) { playbackOrder = new List(); nextBarkSequenceIndex = 0; return false; } playbackOrder = Enumerable.Range(0, barkCount).ToList(); if (shuffleOrder) { for (int i = playbackOrder.Count - 1; i > 0; i--) { int j = Random.Range(0, i + 1); (playbackOrder[i], playbackOrder[j]) = (playbackOrder[j], playbackOrder[i]); } } // If we are resuming mid-sequence (because coroutine was stopped/restarted), // we try to respect the saved index, otherwise reset to 0. if (currentEntryArrayIndex == m_saveData.savedCurrentEntryArrayIndex && shuffleOrder == m_saveData.wasShuffled) { nextBarkSequenceIndex = Mathf.Clamp(m_saveData.savedNextBarkSequenceIndex, 0, playbackOrder.Count); } else { nextBarkSequenceIndex = 0; } return true; } private IEnumerator ContinuousPlaybackLoop() { if (barkManagerEntryIndices == null || barkManagerEntryIndices.Length == 0) { currentPlaybackCoroutine = null; yield break; } while (isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted) { // Safety check: if dialogue started while we were in a yield, break immediately if (DialogueManager.isConversationActive) break; if (currentEntryArrayIndex >= barkManagerEntryIndices.Length) { currentEntryArrayIndex = 0; } int currentManagerEntryIndex = barkManagerEntryIndices[currentEntryArrayIndex]; bool canPlayEntry = PrepareEntrySequence(currentManagerEntryIndex); if (!canPlayEntry) { currentEntryArrayIndex++; m_saveData.savedNextBarkSequenceIndex = 0; yield return null; continue; } while (nextBarkSequenceIndex < playbackOrder.Count && isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted) { // Wait for any other barks (or dialogue) to finish while ((barkManager.IsPlaying || DialogueManager.isConversationActive) && isTriggeringObjectInside && _runtime_IsStarted) { // If dialogue becomes active during this wait, the OnDialogueStarted event // will kill this coroutine, so this check is just a backup. yield return null; } if (!isTriggeringObjectInside || currentTriggererTransform == null || !_runtime_IsStarted) break; int barkIndexToPlay = playbackOrder[nextBarkSequenceIndex]; AudioClip playedClip = barkManager.PlayBark(currentManagerEntryIndex, currentTriggererTransform, barkIndexToPlay); nextBarkSequenceIndex++; // Update Save Data immediately so if we stop mid-sequence we know where we were m_saveData.savedNextBarkSequenceIndex = nextBarkSequenceIndex; float waitTime = delayBetweenBarks; if (playedClip != null) { waitTime += Mathf.Max(0f, playedClip.length); } if (waitTime > 0) { float timer = 0f; while (timer < waitTime && isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted) { if (DialogueManager.isConversationActive) break; // Break timer on dialogue timer += Time.deltaTime; yield return null; } } else { yield return null; } if (DialogueManager.isConversationActive) break; // Exit loop if dialogue started if (!isTriggeringObjectInside || currentTriggererTransform == null || !_runtime_IsStarted) break; } // If we broke out due to dialogue, exit the outer loop properly if (DialogueManager.isConversationActive) break; if (isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted) { currentEntryArrayIndex++; m_saveData.savedNextBarkSequenceIndex = 0; // Reset save data sequence index for next entry m_saveData.savedCurrentEntryArrayIndex = currentEntryArrayIndex; if (currentEntryArrayIndex >= barkManagerEntryIndices.Length) { currentEntryArrayIndex = 0; } if (delayBetweenEntries > 0) { float timer = 0f; while (timer < delayBetweenEntries && isTriggeringObjectInside && currentTriggererTransform != null && _runtime_IsStarted) { if (DialogueManager.isConversationActive) break; timer += Time.deltaTime; yield return null; } if (DialogueManager.isConversationActive) break; if (!isTriggeringObjectInside || currentTriggererTransform == null || !_runtime_IsStarted) break; } } } if (currentPlaybackCoroutine == this.currentPlaybackCoroutine) { currentPlaybackCoroutine = null; } } #endregion #region Public Control Methods [Button] public void StartPlayer() { if (!_runtime_IsStarted) { _runtime_IsStarted = true; if (isInitialized) { CheckForAutoStartIfInside(); } } } [Button] public void StopPlayer() { if (_runtime_IsStarted) { _runtime_IsStarted = false; if (resumeCoroutine != null) StopCoroutine(resumeCoroutine); if (currentPlaybackCoroutine != null) { StopCoroutine(currentPlaybackCoroutine); currentPlaybackCoroutine = null; } } } private void CheckForAutoStartIfInside() { if (_runtime_IsStarted && isTriggeringObjectInside && currentTriggererTransform != null && currentPlaybackCoroutine == null && isInitialized) { // Double check dialogue isn't running before starting if (!DialogueManager.isConversationActive) { currentEntryArrayIndex = Mathf.Clamp(m_saveData.savedCurrentEntryArrayIndex, 0, Mathf.Max(0, barkManagerEntryIndices.Length - 1)); currentPlaybackCoroutine = StartCoroutine(ContinuousPlaybackLoop()); } } } #endregion #region Save System Integration public override string RecordData() { m_saveData.savedIsStarted = this._runtime_IsStarted; m_saveData.savedCurrentEntryArrayIndex = this.currentEntryArrayIndex; m_saveData.savedNextBarkSequenceIndex = this.nextBarkSequenceIndex; m_saveData.wasShuffled = this.shuffleOrder; return SaveSystem.Serialize(m_saveData); } public override void ApplyData(string s) { if (string.IsNullOrEmpty(s)) { _runtime_IsStarted = startAutomaticallyOnLoad; m_saveData = new BarkPlayerData(); m_saveData.savedIsStarted = _runtime_IsStarted; m_saveData.savedCurrentEntryArrayIndex = 0; m_saveData.savedNextBarkSequenceIndex = 0; m_saveData.wasShuffled = shuffleOrder; return; } var loadedData = SaveSystem.Deserialize(s); if (loadedData != null) { m_saveData = loadedData; this._runtime_IsStarted = m_saveData.savedIsStarted; this.currentEntryArrayIndex = m_saveData.savedCurrentEntryArrayIndex; this.nextBarkSequenceIndex = m_saveData.savedNextBarkSequenceIndex; } if (!this._runtime_IsStarted && currentPlaybackCoroutine != null) { StopCoroutine(currentPlaybackCoroutine); currentPlaybackCoroutine = null; } } #endregion #region Helper Methods private float MaxComponent(Vector3 v) => Mathf.Max(Mathf.Max(v.x, v.y), v.z); #endregion } }